July 2025
·3 min read
Core Web Vitals: A Developer's Guide to Web Performance in 2026
Understand LCP, INP, and CLS — Google's Core Web Vitals — and fix the real code causes that tank your scores and hurt your search rankings.
Introduction
- Core Web Vitals are Google's user-experience metrics used as a ranking signal in Search since 2021.
- 2024 update: FID (First Input Delay) was replaced by INP (Interaction to Next Paint) — a harder, more realistic metric.
- The three metrics: LCP (Largest Contentful Paint), INP (Interaction to Next Paint), CLS (Cumulative Layout Shift).
- This guide explains what each measures, what causes poor scores, and how to fix them — with code examples for Next.js.
Measuring Core Web Vitals
- Lighthouse (DevTools → Lighthouse tab): lab data — useful for development iteration.
- CrUX (Chrome User Experience Report): real-user field data — what Google actually uses for ranking.
- PageSpeed Insights (pagespeed.web.dev): combines lab + field data, shows Core Web Vitals by URL.
- Search Console → Core Web Vitals report: aggregated field data for your entire site.
web-vitalsnpm package: measure in real users' browsers and send to analytics.
// Report Web Vitals in Next.js (app/layout.tsx or a Client Component)
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log); // { name: 'LCP', value: 1200, rating: 'good' }
onINP(console.log);
onCLS(console.log);LCP — Largest Contentful Paint
What it measures: time until the largest visible image or text block renders. Good threshold: ≤ 2.5s. Poor threshold: > 4.0s.
Common LCP elements
- Hero images,
<img>, Next.js<Image>. - Large text blocks (h1/h2 without an image above the fold).
- Video poster images.
Fixes
1. Preload the LCP image
// In Next.js, add priority to the hero Image
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // ← adds rel="preload" link + fetchpriority="high"
sizes="100vw"
/>2. Eliminate render-blocking resources
- Move non-critical CSS to async loading.
- Use
next/fontto avoid FOUT (which delays LCP text renders).
3. Use a CDN (Cloudflare, CloudFront)
- LCP is time-to-byte + time-to-render. A CDN edge cache cuts the network portion.
4. Server-side render above-the-fold content
- Don't lazy-load the LCP element — it must be in the initial HTML.
- In Next.js App Router: the LCP element should be in a Server Component, not deferred via
<Suspense>.
INP — Interaction to Next Paint
What it measures: the worst-case time for the browser to visually respond to a user interaction (click, tap, key press) throughout the page's lifetime. Good threshold: ≤ 200ms. Poor threshold: > 500ms.
Common causes of high INP
- Long JavaScript tasks blocking the main thread.
- Heavy event handlers (sort/filter operations on large arrays in the click handler).
- Framework re-renders triggered by state changes during interaction.
Fixes
1. Break up long tasks
// Instead of one big synchronous operation:
function processLargeList(items: Item[]) {
return items.map(heavyTransform); // blocks for 800ms ❌
}
// Yield back to the browser between chunks:
async function processLargeListAsync(items: Item[]) {
const results: Item[] = [];
for (let i = 0; i < items.length; i++) {
results.push(heavyTransform(items[i]));
if (i % 50 === 0) await new Promise(r => setTimeout(r, 0)); // yield ✅
}
return results;
}2. Defer non-critical work
- Use
requestIdleCallbackfor analytics, non-visible DOM updates. - Move heavy computation to a Web Worker.
3. Minimise React re-renders on interaction
- Use
useMemo/useCallbackto prevent child re-renders. - Batch state updates (React 18+ auto-batches inside event handlers).
CLS — Cumulative Layout Shift
What it measures: the total unexpected layout movement visible to the user. Good threshold: ≤ 0.1. Poor threshold: > 0.25.
Common CLS culprits
- Images without explicit
width/height(browser doesn't reserve space). - Dynamically injected content above existing content (banners, cookie notices).
- Late-loading fonts (FOUT).
- Async-loaded components that push content down.
Fixes
1. Always set image dimensions
// Next.js <Image> requires width/height — reserves space automatically
<Image src="/blog-cover.jpg" alt="..." width={800} height={450} />
// For flexible-width images, use fill with a parent container
<div style={{ position: 'relative', aspectRatio: '16/9' }}>
<Image src="/blog-cover.jpg" alt="..." fill sizes="(max-width: 768px) 100vw, 50vw" />
</div>2. Reserve space for dynamic content
/* Cookie banner — always reserve height so it doesn't push content */
.cookie-banner {
min-height: 64px;
}3. Use next/font
- Fonts loaded with
next/fontare preloaded and self-hosted — zero layout shift from font swap.
Priority Quick-Reference
| Metric | First fix | Second fix |
|---|---|---|
| High LCP | Add priority to hero <Image> | Move to CDN edge |
| High INP | Find long tasks in Profiler | Yield or Web Worker |
| High CLS | Add width/height to all images | Use next/font |
Continuous Monitoring
- Set up real-user monitoring with
web-vitals→ send to your analytics (GA4, Datadog, PostHog). - CI regression guard: run Lighthouse CI (
lhci autorun) on PRs; fail if LCP degrades >10%. - Google Search Console Core Web Vitals report: check monthly for regressions on indexed pages.
Conclusion
- LCP is the most impactful metric to fix first — hero image + CDN usually gets you to "Good".
- INP is the trickiest: requires real-user data to reproduce and profiling to fix.
- CLS is the most preventable: set image dimensions and use
next/fontfrom day one. - "Good" on all three (field data) is a ranking differentiator in competitive SERPs.