Core Web Vitals in 2026: The Complete Developer Guide
Everything developers need to know about Core Web Vitals in 2026 — LCP, INP, CLS explained with real-world fixes and measurement strategies.
Muhammad Qasim
Senior Full Stack Developer
Core Web Vitals Are Now a Direct Ranking Factor
Google confirmed it. Core Web Vitals (CWV) are part of the page experience signal used for ranking. If your competitors have good CWV scores and you don't, they're likely outranking you — regardless of content quality.
The three metrics you need to nail:
| Metric | Good | Needs Work | Poor | |--------|------|------------|------| | LCP (Largest Contentful Paint) | < 2.5s | 2.5–4s | > 4s | | INP (Interaction to Next Paint) | < 200ms | 200–500ms | > 500ms | | CLS (Cumulative Layout Shift) | < 0.1 | 0.1–0.25 | > 0.25 |
Note: INP replaced FID (First Input Delay) in March 2024. If your audits still reference FID, they're outdated.
LCP: Largest Contentful Paint
LCP measures how long it takes for the largest visible element (usually a hero image or headline) to render.
Common LCP Culprits
1. Unoptimized hero images
<!-- Bad: no dimensions, no lazy loading control, wrong format -->
<img src="hero.jpg" alt="Hero">
<!-- Good: explicit dims, eager loading, WebP, fetchpriority -->
<img
src="hero.webp"
alt="Hero — senior developer at work"
width="1200"
height="600"
loading="eager"
fetchpriority="high"
decoding="async"
>
In Next.js, use next/image with priority prop:
import Image from 'next/image'
<Image
src="/hero.webp"
alt="Muhammad Qasim — Senior Full Stack Developer"
width={1200}
height={600}
priority
/>
2. Slow server response (TTFB)
Target: < 800ms TTFB.
- Use a CDN (Cloudflare, Vercel Edge)
- Enable server-side caching
- Move to a faster hosting provider
3. Render-blocking resources
Any script or CSS in <head> that blocks rendering delays LCP.
<!-- Bad: blocks rendering -->
<script src="analytics.js"></script>
<!-- Good: defer non-critical scripts -->
<script src="analytics.js" defer></script>
INP: Interaction to Next Paint
INP replaced FID and is stricter — it measures all interactions during the page lifetime, not just the first.
Fixing Poor INP
1. Break up long tasks
JavaScript tasks > 50ms block the main thread. Use scheduler.yield():
async function processLargeData(items: Item[]) {
for (const item of items) {
processItem(item)
// Yield to browser between chunks
if (shouldYield()) await scheduler.yield()
}
}
2. Use React's startTransition for non-urgent updates
import { startTransition } from 'react'
function SearchInput() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setQuery(value) // Urgent: update input immediately
startTransition(() => {
setResults(filterResults(value)) // Non-urgent: can be interrupted
})
}
return <input value={query} onChange={handleChange} />
}
3. Reduce JavaScript bundle size
# Analyze your bundle
npx @next/bundle-analyzer
Common culprits: large utility libraries (lodash → use native JS), moment.js (→ use date-fns), and icon libraries (import individual icons, not entire packs).
CLS: Cumulative Layout Shift
CLS measures unexpected layout movement. The most common causes:
1. Images without dimensions
<!-- CLS: no width/height causes layout shift -->
<img src="photo.jpg" alt="...">
<!-- No CLS: browser reserves space -->
<img src="photo.jpg" alt="..." width="800" height="450">
2. Late-loading fonts (FOUT/FOIT)
/* Reserve space for custom fonts */
@font-face {
font-family: 'MyFont';
font-display: optional; /* No layout shift at all */
}
3. Dynamic content injection
If you inject ads, banners, or notifications into the page, reserve space:
.ad-slot {
min-height: 250px; /* Reserve space before ad loads */
}
Measuring CWV in Production
Lab data (PageSpeed Insights, Lighthouse) shows synthetic performance. Field data (real users) is what Google uses for ranking.
Tools:
- Google Search Console → Core Web Vitals report (real data)
- Chrome UX Report → API for CrUX data
- web-vitals npm package → Measure in your app
import { onLCP, onINP, onCLS } from 'web-vitals'
function sendToAnalytics(metric: Metric) {
// Send to GA4, Datadog, etc.
gtag('event', metric.name, {
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
metric_id: metric.id,
metric_value: metric.value,
metric_delta: metric.delta,
})
}
onLCP(sendToAnalytics)
onINP(sendToAnalytics)
onCLS(sendToAnalytics)
My CWV Audit Process
When I audit a client site, here's the exact order:
- Run PageSpeed Insights — identify failing metrics
- Check LCP element — is it an image? Fix loading strategy
- Check for render-blocking resources — defer or eliminate
- Measure TTFB — if > 800ms, hosting/caching issue
- Check CLS — add dimensions to all images/embeds
- Profile JavaScript — find and break up long tasks
- Verify on real devices — mobile often worse than desktop
After fixing all issues, I rerun and confirm field data improves in Search Console over 28 days.
Real Results
For a recent client (React marketing site):
- LCP: 4.8s → 1.4s (71% improvement)
- INP: 380ms → 80ms (79% improvement)
- CLS: 0.28 → 0.03 (89% improvement)
Result: first page rankings for their primary keyword within 6 weeks.
Need a CWV audit? My performance optimization service starts with a comprehensive Core Web Vitals report and fixes every identified issue. Book an audit →
Need Help With This?
I offer professional web development services — WordPress, React/Next.js, performance optimization, and technical SEO.
Get in Touch