QuestionsWeb Performance

Core Web Vitals: LCP, INP, CLS

Core Web VitalsEasyWeb Performance

Name the three Core Web Vitals, what each measures, and the 'good' threshold for each.

What it tests

Knowing the user-centric metrics Google standardized on and their target values.

Approach & answer

Core Web Vitals are three field metrics, each capturing a distinct part of the experience. LCP — Largest Contentful Paint — measures loading: the time until the largest visible element (usually the hero image or headline block) is rendered. Good is ≤ 2.5s (at the 75th percentile of real users). INP — Interaction to Next Paint — measures responsiveness: across the whole visit it takes the worst (near-worst) latency from a user interaction (tap, click, keypress) to the next frame the browser paints in response. Good is ≤ 200ms. INP replaced FID (First Input Delay) in March 2024 because FID only measured the delay of the FIRST interaction and only its input delay, whereas INP measures every interaction end-to-end. CLS — Cumulative Layout Shift — measures visual stability: a unitless score summing how much visible content unexpectedly jumps around (an image loading with no reserved space, an ad pushing text down). Good is ≤ 0.1. The threshold to remember for each: 2.5s / 200ms / 0.1, all at p75 of real-user data. Each maps to a different fix: LCP → prioritize the hero resource and cut render-blocking; INP → break up long tasks and yield to the main thread; CLS → reserve space for anything that loads or moves.

Use this technique when

Setting performance budgets; interpreting a Lighthouse or CrUX report.

Code

// Measure the vitals in real users with the web-vitals library idea,
// or directly with PerformanceObserver:
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // entry.startTime is the LCP render time (ms)
    console.log('LCP candidate:', entry.startTime, entry.element);
  }
}).observe({ type: 'largest-contentful-paint', buffered: true });

// CLS: sum layout-shift entries that weren't caused by recent input
let cls = 0;
new PerformanceObserver((list) => {
  for (const e of list.getEntries()) if (!e.hadRecentInput) cls += e.value;
}).observe({ type: 'layout-shift', buffered: true });

References