Core Web Vitals measure real user experience: how fast content appears, how quickly the page responds to interaction, and how stable the layout is. Google uses these metrics as ranking signals. More importantly, good scores mean better user experience.
The three Core Web Vitals
| Metric | Measures | Good | Needs Improvement | Poor |
|---|---|---|---|---|
| LCP | Loading performance | ≤2.5s | 2.5-4s | >4s |
| INP | Interactivity | ≤200ms | 200-500ms | >500ms |
| CLS | Visual stability | ≤0.1 | 0.1-0.25 | >0.25 |
Largest Contentful Paint (LCP)
LCP measures when the largest visible content element (image, video, text block) finishes rendering. It approximates when users perceive the page as "loaded."
Common LCP candidates:
- Hero images
- Large text blocks
- Video poster images
- Background images
Interaction to Next Paint (INP)
INP measures responsiveness by observing all click, tap, and keyboard interactions throughout the page's lifecycle. It reports the worst interaction (with outliers excluded).
Unlike FID (which measured only the first interaction), INP captures ongoing responsiveness.
Cumulative Layout Shift (CLS)
CLS measures visual stability—how much content shifts unexpectedly. Each time visible elements move after rendering, it adds to the CLS score.
Common causes:
- Images without dimensions
- Ads and embeds without reserved space
- Dynamically injected content
- Web fonts causing text reflow
Measuring Web Vitals
Lab data (controlled environment)
| Tool | Best for |
|---|---|
| Lighthouse | Auditing and debugging |
| Chrome DevTools Performance | Detailed analysis |
| PageSpeed Insights | Quick scores |
| WebPageTest | Advanced testing |
Field data (real users)
| Source | Access |
|---|---|
| Chrome UX Report (CrUX) | PageSpeed Insights, BigQuery |
| web-vitals library | Your analytics |
| Google Search Console | Your site's data |
| Vercel Analytics | Next.js projects |
Field data reflects real user experience across different devices and networks.
Using the web-vitals library
npm install web-vitals
import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
delta: metric.delta,
});
navigator.sendBeacon('/api/analytics', body);
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
Next.js Web Vitals
// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next';
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<SpeedInsights />
</body>
</html>
);
}
Or use the custom reporter:
// pages/_app.tsx
export function reportWebVitals(metric) {
console.log(metric);
// Send to analytics
}
Improving LCP
Identify the LCP element
- Open Chrome DevTools
- Run Lighthouse or Performance audit
- Look for "Largest Contentful Paint element"
Optimization strategies
| Issue | Fix |
|---|---|
| Slow server response | Use CDN, optimize backend |
| Render-blocking resources | Defer non-critical JS/CSS |
| Large images | Compress, use modern formats |
| Client-side rendering | Use SSR or SSG |
| Slow font loading | Preload critical fonts |
Prioritize LCP image
<!-- HTML -->
<img src="hero.jpg" fetchpriority="high" />
<!-- Next.js -->
<Image src="/hero.jpg" priority />
Preload critical resources
<link rel="preload" href="/hero.jpg" as="image" />
<link rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin />
Improving INP
Identify slow interactions
Use Chrome DevTools Performance panel:
- Record interactions
- Look for long tasks (>50ms)
- Identify blocking JavaScript
Optimization strategies
| Issue | Fix |
|---|---|
| Long tasks | Break into smaller chunks |
| Heavy JavaScript | Code split, lazy load |
| Layout thrashing | Batch DOM reads/writes |
| Third-party scripts | Load async, defer |
Yield to main thread
// Break up long work
async function processItems(items) {
for (const item of items) {
process(item);
// Yield every 50ms
if (performance.now() - start > 50) {
await new Promise(resolve => setTimeout(resolve, 0));
start = performance.now();
}
}
}
Use web workers for heavy computation
// worker.ts
self.onmessage = (e) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url));
worker.postMessage(data);
worker.onmessage = (e) => handleResult(e.data);
Improving CLS
Set explicit dimensions
<!-- Always include width and height -->
<img src="photo.jpg" width="800" height="600" alt="..." />
<!-- Or use aspect-ratio -->
<img src="photo.jpg" style="aspect-ratio: 4/3; width: 100%;" alt="..." />
Reserve space for dynamic content
/* Reserve space for ad */
.ad-container {
min-height: 250px;
}
/* Skeleton for loading content */
.skeleton {
height: 200px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
}
Handle web fonts
/* Prevent layout shift from font loading */
@font-face {
font-family: 'CustomFont';
src: url('/font.woff2') format('woff2');
font-display: swap;
}
/* Or optional for non-critical fonts */
@font-face {
font-family: 'DecorativeFont';
font-display: optional;
}
Avoid inserting content above viewport
// Bad: shifts content down
container.prepend(newElement);
// Better: insert below or use animation
container.append(newElement);
Debugging specific issues
Find layout shifts
// Log all layout shifts
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('Layout shift:', entry);
console.log('Sources:', entry.sources);
}
}).observe({ type: 'layout-shift', buffered: true });
Find long tasks
// Log long tasks
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('Long task:', entry.duration, 'ms');
}
}).observe({ type: 'longtask' });
Setting up monitoring
Custom dashboard
// Aggregate and track over time
function reportVitals(metric) {
fetch('/api/vitals', {
method: 'POST',
body: JSON.stringify({
metric: metric.name,
value: metric.value,
page: window.location.pathname,
connection: navigator.connection?.effectiveType,
}),
});
}
Alerting thresholds
| Metric | Alert threshold |
|---|---|
| LCP | p75 > 2.5s |
| INP | p75 > 200ms |
| CLS | p75 > 0.1 |
Alert on 75th percentile to catch issues affecting most users.
Summary
Core Web Vitals measure loading (LCP), interactivity (INP), and stability (CLS). Measure with lab tools for debugging and field data for real user experience. Improve LCP with image optimization and preloading. Improve INP by breaking up long tasks and deferring heavy JavaScript. Improve CLS with explicit dimensions and reserved space. Monitor continuously to catch regressions.
