Mark Lowel Montealto — Full Stack Developer & DevOps Engineer

Mark Lowel
Montealto

Full Stack Developer & DevOps Engineer

Blog

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-vitals npm 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/font to 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 requestIdleCallback for analytics, non-visible DOM updates.
  • Move heavy computation to a Web Worker.

3. Minimise React re-renders on interaction

  • Use useMemo/useCallback to 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/font are preloaded and self-hosted — zero layout shift from font swap.

Priority Quick-Reference

MetricFirst fixSecond fix
High LCPAdd priority to hero <Image>Move to CDN edge
High INPFind long tasks in ProfilerYield or Web Worker
High CLSAdd width/height to all imagesUse 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/font from day one.
  • "Good" on all three (field data) is a ranking differentiator in competitive SERPs.