D
Dev SOPKnowledge Base
Search
← All topics

Core Web Vitals Optimization: LCP, INP, and CLS

Definitive Core Web Vitals reference covering LCP, INP, and CLS thresholds, diagnosis flow, and concrete optimizations for Next.js applications.

performancecore-web-vitalslcpinpclsnextjsseo
Agent trigger phrases: Core Web Vitals · LCP optimization · INP optimization · CLS fix · Largest Contentful Paint · Interaction to Next Paint · Cumulative Layout Shift · page speed

Overview

Core Web Vitals are Google's primary user-experience metrics. They directly affect SEO rankings and user retention.

The Three Metrics

| Metric | Full Name | 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 |

LCP = how fast the main content appears. INP = how responsive the page is to clicks/taps/keys. CLS = how much content shifts unexpectedly during load.

Diagnosis Flow

1. Run PageSpeed Insights (pagespeed.web.dev)
   → Identifies which metric(s) are failing

2. Run Chrome DevTools Performance tab
   → See the exact LCP element, INP interaction, or CLS source

3. Fix the root cause (not symptoms)

4. Re-measure — CWV changes take ~28 days to reflect in Search Console

LCP Optimization

The LCP element is almost always: hero image, above-the-fold heading, or video poster.

<!-- Priority: preload LCP image -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
// Next.js Image component — LCP hero
<Image
  src="/hero.webp"
  alt="Hero"
  width={1200}
  height={600}
  priority          // Removes lazy loading for LCP element
  fetchPriority="high"
  sizes="(max-width: 768px) 100vw, 1200px"
/>

LCP root causes and fixes:

| Root Cause | Fix | |------------|-----| | Large unoptimized image | Convert to WebP/AVIF, compress, use <Image> | | Image not preloaded | Add priority or <link rel="preload"> | | Render-blocking resources | Defer non-critical JS, inline critical CSS | | Slow TTFB | CDN, edge caching, reduce server response time | | No resource hints | Add dns-prefetch and preconnect for 3rd-party origins |

<!-- Preconnect to critical third-party origins -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

INP Optimization

INP replaced FID in March 2024. It measures the worst-case interaction delay, not just the first.

// Break up long tasks with scheduler.yield()
async function processLargeList(items: Item[]) {
  for (let i = 0; i < items.length; i++) {
    processItem(items[i]);
    if (i % 50 === 0) {
      await scheduler.yield();  // Yield to browser between chunks
    }
  }
}

// Defer non-critical work
function handleClick() {
  updateUI();              // Critical — do immediately
  setTimeout(() => {       // Defer analytics/logging
    logEvent("click");
  }, 0);
}

INP root causes and fixes:

| Root Cause | Fix | |------------|-----| | Long tasks (> 50ms) | Break with scheduler.yield() or setTimeout | | Hydration jank | Reduce JS bundle, use React useTransition | | Synchronous 3rd-party scripts | Load async/defer or move to Web Worker | | Heavy event handlers | Debounce, throttle, or move work off main thread |

CLS Optimization

CLS is caused by content shifting after initial render — images without dimensions, dynamically injected content, web fonts swapping.

/* Reserve space for images */
img, video {
  aspect-ratio: attr(width) / attr(height);  /* Modern approach */
}

/* Font swap without layout shift */
@font-face {
  font-family: "Inter";
  font-display: optional;  /* "swap" causes CLS; "optional" doesn't */
}

/* Reserve space for dynamic content */
.ad-slot {
  min-height: 250px;  /* Known ad size */
}
// Next.js Image — always specify width/height
<Image src="/photo.jpg" alt="" width={800} height={600} />

// Skeleton placeholder — prevents shift
{isLoading ? <Skeleton className="h-48 w-full" /> : <Content />}

Quick Wins Checklist

  • [ ] LCP element has priority or fetchpriority="high"
  • [ ] All images have explicit width and height
  • [ ] Fonts use font-display: optional (or swap with explicit dimensions)
  • [ ] No layout shifts from injected ads/embeds (reserved space)
  • [ ] No long tasks > 50ms in click handlers
  • [ ] Third-party scripts loaded with async or defer
  • [ ] Critical CSS inlined, non-critical deferred
  • [ ] TTFB < 600ms (CDN, caching, edge functions)

Measurement Tools

| Tool | Best For | |------|---------| | PageSpeed Insights | Lab + field data, actionable recommendations | | Chrome DevTools Performance | Deep LCP/INP diagnosis, flame charts | | Search Console Core Web Vitals | Real user data over 28-day window | | web-vitals npm package | Real user monitoring in production |

// src/lib/vitals.ts — Report to analytics
import { onLCP, onINP, onCLS } from "web-vitals";

onLCP(metric => sendToAnalytics("LCP", metric.value));
onINP(metric => sendToAnalytics("INP", metric.value));
onCLS(metric => sendToAnalytics("CLS", metric.value));