Core Web Vitals are Google's set of performance metrics that directly affect search ranking. More importantly, they measure real user experience: whether your page loads visibly fast, responds quickly to interaction, and stays stable as it renders.
The three metrics, what they measure, and how to improve them.
The Three Core Web Vitals
LCP — Largest Contentful Paint
Measures when the largest visible element (hero image, h1, article body) finishes rendering. The goal is under 2.5 seconds.
LCP is the "does the page look done?" metric. It's what users notice when they're waiting — the blank space before the main content appears.
What hurts LCP:
- Large, unoptimized hero images
- Server response time (TTFB)
- Render-blocking scripts and stylesheets
- Client-side rendering (JS renders the content)
How to improve LCP:
Optimize the LCP element itself:
<!-- Preload the LCP image -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">
<!-- Use modern formats -->
<picture>
<source srcset="/hero.avif" type="image/avif">
<source srcset="/hero.webp" type="image/webp">
<img src="/hero.jpg" alt="Hero" width="1200" height="600">
</picture>
Remove render-blocking resources:
<!-- Non-critical CSS: load async -->
<link rel="preload" href="/styles/non-critical.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<!-- Defer non-critical scripts -->
<script src="/analytics.js" defer></script>
INP — Interaction to Next Paint (replaced FID in 2024)
Measures the latency between user interaction (click, tap, keypress) and the next visual update. Goal: under 200ms.
INP replaced First Input Delay (FID) in March 2024. FID only measured the first interaction; INP measures all interactions throughout the page lifecycle.
What hurts INP:
- Long JavaScript tasks blocking the main thread
- Heavy event handlers
- Synchronous layout reads during interaction (layout thrashing)
How to improve INP:
Break up long tasks:
// Bad: one long synchronous task
function processLargeList(items) {
items.forEach(item => heavyOperation(item));
}
// Better: yield to the browser between chunks
async function processLargeList(items) {
const CHUNK_SIZE = 50;
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
chunk.forEach(item => heavyOperation(item));
await new Promise(r => setTimeout(r, 0)); // yield
}
}
Use scheduler.yield() when available (Chrome 115+, proposal stage for others):
async function handleClick() {
doUrgentWork();
await scheduler.yield(); // let browser render first
doNonUrgentWork();
}
Avoid layout thrashing:
// Bad: alternating read/write forces multiple layout recalculations
elements.forEach(el => {
const height = el.offsetHeight; // read
el.style.height = height + 10 + 'px'; // write → forces layout
});
// Good: batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight); // all reads
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // all writes
});
CLS — Cumulative Layout Shift
Measures unexpected visual shifts — elements jumping around as the page loads. Goal: under 0.1.
CLS is the "why did the button move when I was about to click it?" metric.
What causes CLS:
- Images without dimensions
- Ads, embeds, iframes without reserved space
- Dynamically injected content above existing content
- Web fonts causing FOIT/FOUT
How to fix CLS:
Always set image dimensions:
<!-- Reserve space before image loads -->
<img src="photo.jpg" width="800" height="450" alt="...">
<!-- Or use CSS aspect-ratio -->
<style>
.image-wrapper {
aspect-ratio: 16 / 9;
width: 100%;
}
</style>
Reserve space for ads and embeds:
.ad-slot {
min-height: 250px; /* reserve before ad loads */
}
Prevent font-swap layout shift:
@font-face {
font-family: 'MyFont';
src: url('myfont.woff2') format('woff2');
font-display: optional; /* don't show fallback if font loads quickly */
}
Or use size-adjust to match fallback font metrics:
@font-face {
font-family: 'FallbackFont';
src: local('Arial');
size-adjust: 103%; /* scale fallback to match custom font */
ascent-override: 90%;
}
Measuring Core Web Vitals
In Firefox DevTools
Firefox's Performance profiler doesn't show CWV directly, but you can measure:
- LCP: Watch the timeline for when the largest paint fires
- CLS: Check Layout panel for unexpected reflows
- INP: Performance recording shows main thread blocking during interaction
For accurate field measurements, use PageSpeed Insights (which uses real Chrome User Experience data) or install the Web Vitals extension.
In Code (JavaScript)
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(metric => {
console.log('LCP:', metric.value, metric.rating); // 'good', 'needs-improvement', 'poor'
});
onINP(metric => {
console.log('INP:', metric.value);
});
onCLS(metric => {
console.log('CLS:', metric.value);
});
The web-vitals library (from Google) is the reference implementation.
Lighthouse
Run from Chrome DevTools → Lighthouse tab, or via CLI:
npx lighthouse https://yoursite.com --output=html --view
Lighthouse scores are lab measurements (controlled environment). They don't match field data (real users on varied connections). Both matter — lab for reproducible testing, field for actual impact.
Priority Order for Fixes
- LCP first — it's the most visible to users and usually has the biggest impact
- CLS second — easy to fix (add image dimensions) and directly affects usability
- INP last — hardest to fix, requires JS profiling, but matters for interactive apps
Quick LCP Checklist
- [ ] LCP element is an image? Preload it with
fetchpriority="high" - [ ] Image in WebP or AVIF format?
- [ ] Image properly sized (not 4000px wide for a 800px slot)?
- [ ] Server response under 800ms? (Check TTFB in Network panel)
- [ ] No render-blocking scripts before the LCP element?
- [ ] CDN serving static assets?
Quick CLS Checklist
- [ ] All images have
widthandheightattributes? - [ ] Ad slots have
min-heightreserved? - [ ] No content injected above the fold after initial render?
- [ ] Font
font-display: optionalorswapwithsize-adjust?
Core Web Vitals scores are now a Google search ranking factor — not the dominant one, but measurable. More importantly, improving them correlates with higher conversion rates: Google's data shows that sites meeting "Good" thresholds for all three metrics have significantly lower bounce rates than those that don't.
Top comments (0)