Back to Performance
Performance
medium
very high
mid

What are the Core Web Vitals (LCP, INP, CLS) and how do you optimize each?

LCP measures loading (largest paint), INP measures interaction responsiveness (replaced FID in 2024), CLS measures layout stability. Optimize each with different levers: LCP via image/critical-resource pipeline, INP via task scheduling, CLS via reserving space.

7 min read·~20 min to think through

Three Core Web Vitals as of 2024+:

LCP — Largest Contentful Paint (≤ 2.5s for "good"). Time to paint the largest above-the-fold element (often the hero image or H1). Levers:

  • Compress + size the LCP image (AVIF/WebP, srcset, fetchpriority="high").
  • Eliminate render-blocking CSS/JS; inline critical CSS for above-the-fold.
  • Use a CDN; static-render the page if possible.
  • Preconnect to the origin serving the LCP element.

INP — Interaction to Next Paint (≤ 200ms). Replaced FID in March 2024. Worst-case latency between any user input and the next paint. Levers:

  • Break long tasks (>50ms) with yieldToMain / scheduler.postTask.
  • Move CPU work off the main thread with Web Workers.
  • Defer non-critical work (analytics, ads) until after interaction.
  • Avoid layout thrash inside event handlers (read-then-write batched).

CLS — Cumulative Layout Shift (≤ 0.1). Sum of unexpected layout shifts during the page lifetime. Levers:

  • Always set width/height on images and <video> (lets the browser reserve space).
  • Reserve space for ads/embeds.
  • Avoid inserting content above existing content (especially banners).
  • Use font-display: optional or size-adjust to avoid text reflow on font load.

Measure with the web-vitals library (real users) and Lighthouse / PageSpeed Insights (lab). Real-user numbers from CrUX are what Google ranks on.

Code

tsx
<img
  src="/hero.webp"
  width={1200}
  height={628}      // sets aspect-ratio, no layout shift on load
  fetchpriority="high"
  loading="eager"
  alt="hero"
/>
Reserving space for an image (CLS-safe)
ts
async function processChunked<T>(items: T[], step: (x: T) => void) {
  for (let i = 0; i < items.length; i++) {
    step(items[i]);
    if (i % 50 === 0) await new Promise<void>((r) => setTimeout(r, 0));
  }
}
Yield long tasks to keep INP healthy

Follow-up questions

  • Why was FID replaced by INP?
  • How do you investigate a 'long task' shown in DevTools?
  • How does third-party JS typically degrade CLS and INP?

Common mistakes

  • Optimizing the wrong vital — improving LCP when INP is the user complaint.
  • Trusting Lighthouse alone; field data (CrUX) is what Google ranks on.
  • Lazy-loading the LCP image — adds a round trip and tanks LCP.

Performance considerations

  • Server-side render the LCP element as plain HTML — avoid hydration on the critical path.
  • Defer hydration of below-the-fold islands (Astro, RSC, partial hydration).

Edge cases

  • Single-page apps need to instrument soft navigations explicitly — built-in vitals only cover the initial nav.
  • INP samples the *worst* interaction; one bad click ruins the page's score.

Real-world examples

  • Vercel/Next dashboards expose INP regression alerts on PRs — caught before users complain.

Senior engineer discussion

Senior signal: discuss budget-based perf gates in CI, RUM vs lab, and the cost-of-CO2 angle (smaller bundles, less CPU on user devices).

Related questions