Core Web Vitals
Core Web Vitals are three metrics Google uses to measure real-world user experience. They affect search rankings and represent what users actually care about: how fast the page loads, how quickly it responds, and whether things jump around.
LCP: Largest Contentful Paint
LCP measures how fast the main content of the page becomes visible. It tracks the render time of the largest image, video, or text block in the viewport.
What Counts as LCP
<img>elements<video>poster images- Elements with a CSS
background-image - Block-level elements containing text
Target Scores
| Rating | LCP Time |
|---|---|
| Good | Under 2.5 seconds |
| Needs Improvement | 2.5 to 4.0 seconds |
| Poor | Over 4.0 seconds |
Common LCP Problems
<!-- Problem: hero image loads late because it's lazy loaded -->
<img src="hero.jpg" loading="lazy" alt="Hero banner" />
<!-- Fix: never lazy load the LCP element -->
<img src="hero.jpg" alt="Hero banner" fetchpriority="high" />
Improving LCP
- Optimize the critical path: the LCP element should load as early as possible
- Preload the LCP image: tell the browser about it immediately
<link rel="preload" as="image" href="/hero.jpg" fetchpriority="high" />
- Avoid render-blocking resources: CSS and synchronous JS delay rendering
- Use a CDN: serve assets from a location close to the user
- Optimize images: use modern formats (WebP, AVIF), serve responsive sizes
<img
src="hero.webp"
srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w"
sizes="100vw"
alt="Hero"
fetchpriority="high"
/>
INP: Interaction to Next Paint
INP (Interaction to Next Paint) replaced FID (First Input Delay) in March 2024. While FID only measured the delay of the first interaction, INP measures responsiveness throughout the entire page lifecycle.
How INP Works
INP tracks the latency of all clicks, taps, and keyboard interactions during the page visit. The final INP value is the worst interaction (with some outlier exclusion).
Each interaction has three phases:
- Input delay: time from the user's action until the event handler starts
- Processing time: time spent running the event handler
- Presentation delay: time for the browser to render the next frame
Target Scores
| Rating | INP Time |
|---|---|
| Good | Under 200 milliseconds |
| Needs Improvement | 200 to 500 milliseconds |
| Poor | Over 500 milliseconds |
Common INP Problems
// Problem: heavy computation blocks the main thread
button.addEventListener('click', () => {
const result = processMillionRecords(data); // Blocks for 800ms
displayResult(result);
});
// Fix: break work into smaller chunks
button.addEventListener('click', async () => {
showSpinner();
const result = await processInChunks(data);
displayResult(result);
});
async function processInChunks(data) {
const chunks = splitIntoChunks(data, 1000);
const results = [];
for (const chunk of chunks) {
results.push(processChunk(chunk));
// Yield to the main thread between chunks
await new Promise((resolve) => setTimeout(resolve, 0));
}
return results.flat();
}
Improving INP
- Keep event handlers fast: under 50ms ideally
- Yield to the main thread: break long tasks into smaller pieces
- Use Web Workers: move heavy computation off the main thread
- Minimize third-party scripts: each script competes for main thread time
- Debounce frequent events: scroll, resize, and input handlers
CLS: Cumulative Layout Shift
CLS measures visual stability. It tracks how much visible content unexpectedly shifts during the page's lifetime.
How CLS Is Calculated
CLS = sum of all unexpected layout shift scores during the page session. Each shift score = impact fraction x distance fraction.
- Impact fraction: percentage of the viewport affected by the shift
- Distance fraction: how far the element moved (as a fraction of the viewport)
Target Scores
| Rating | CLS Score |
|---|---|
| Good | Under 0.1 |
| Needs Improvement | 0.1 to 0.25 |
| Poor | Over 0.25 |
Common CLS Problems
<!-- Problem: image without dimensions causes layout shift -->
<img src="photo.jpg" alt="Photo" />
<!-- Fix: always specify width and height -->
<img src="photo.jpg" alt="Photo" width="800" height="600" />
/* Even better: use aspect-ratio */
img {
width: 100%;
height: auto;
aspect-ratio: 4 / 3;
}
Top Causes of Layout Shift
- Images without dimensions: the browser does not know the size until the image loads
- Ads and embeds: injected content pushes existing content down
- Dynamically injected content: banners, cookie notices, notifications
- Web fonts causing FOUT: text resizes when the custom font loads
- Late-loading CSS: styles applied after initial render reflow the page
Preventing CLS
/* Reserve space for ads */
.ad-slot {
min-height: 250px;
width: 300px;
}
/* Prevent font swap layout shift */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: optional; /* No FOUT, no layout shift */
}
<!-- Reserve space for dynamic content -->
<div class="notification-area" style="min-height: 60px;">
<!-- Notification will be injected here -->
</div>
Measuring Core Web Vitals
In the Field (Real User Data)
- Chrome User Experience Report (CrUX): real data from Chrome users
- PageSpeed Insights: shows both field data and lab data
- Google Search Console: Core Web Vitals report for your entire site
web-vitalslibrary: measure in your own analytics
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP((metric) => {
console.log('LCP:', metric.value, 'ms');
sendToAnalytics('LCP', metric.value);
});
onINP((metric) => {
console.log('INP:', metric.value, 'ms');
sendToAnalytics('INP', metric.value);
});
onCLS((metric) => {
console.log('CLS:', metric.value);
sendToAnalytics('CLS', metric.value);
});
In the Lab (Development)
- Lighthouse: built into Chrome DevTools (Audits tab)
- Chrome DevTools Performance tab: record and inspect individual interactions
- WebPageTest: detailed waterfall analysis from multiple locations
Using Chrome DevTools
- Open DevTools (F12)
- Go to the Performance tab
- Click Record, interact with the page, stop recording
- Look for long tasks (red bars), layout shifts, and slow paints
- The Performance Insights panel highlights CWV issues
Google Uses These for Ranking
Since 2021, Core Web Vitals are a ranking signal in Google Search. Pages with good CWV get a minor ranking boost. Pages with poor CWV may be penalized.
The threshold is not dramatic. Content quality still matters far more. But between two pages with equal content, the faster one wins.
What Google Measures
Google uses field data (real Chrome user data from CrUX), not lab data. Your Lighthouse score is a development tool, not what Google ranks on. Optimize for the 75th percentile of real user visits.
Common Pitfalls
- Optimizing Lighthouse instead of real users: a perfect Lighthouse score does not guarantee good field metrics. Real users have slow devices and networks.
- Lazy loading the LCP element: the largest content element should load as early as possible. Use
fetchpriority="high"and preload hints instead. - Ignoring CLS after initial load: CLS accumulates throughout the session. Late-injecting content (cookie banners, popups) counts.
- Measuring only on fast devices: Core Web Vitals reflect the 75th percentile. Test on mid-range devices and slow connections.
- Forgetting about third-party scripts: analytics, ads, and chat widgets add to main thread work and hurt INP.
- Not reserving space for dynamic content: ads, images, and embeds must have pre-defined dimensions.
Key Takeaways
- LCP measures loading speed (target: under 2.5 seconds)
- INP measures interactivity responsiveness (target: under 200 milliseconds)
- CLS measures visual stability (target: under 0.1)
- Google uses real user data at the 75th percentile for ranking
- Measure in the field with CrUX or the web-vitals library, not just Lighthouse
- The biggest LCP wins come from preloading images and removing render-blocking resources
- The biggest INP wins come from keeping event handlers fast and yielding to the main thread
- The biggest CLS wins come from specifying image dimensions and reserving space for dynamic content