ESC

Type to search the knowledge base.

Core Web Vitals

LCP, INP, and CLS — what field data measures, good thresholds, and fixes that actually move the needle (per web.dev guidance).

intermediate4 min read
  • performance
  • web-vitals
  • lcp
  • inp
  • cls

Core Web Vitals are Google’s field-oriented UX metrics. Lab tools (Lighthouse) help you debug. Production RUM / CrUX is what tells you if real users are fine.

Measure the 75th percentile, mobile and desktop separately. One fast laptop on Wi‑Fi is not a product.

The three (today)

Metric Question it answers “Good”
LCP How fast does the main content show up? ≤ 2.5s
INP How responsive is the page to clicks/taps/keys over the whole visit? ≤ 200ms
CLS How much does the layout jump around unexpectedly? ≤ 0.1

FID is retired. INP replaced it as the interactivity Core Web Vital.

LCP — largest contentful paint

Usually a hero image, big text block, or video poster. Users decide “is this useful?” around here.

Fixes that web.dev keeps repeating for a reason:

  1. Make the LCP resource discoverable in the initial HTML (not injected late by JS).
  2. Prioritize it — fetchpriority="high", preload when needed, don’t loading="lazy" the LCP image.
  3. Shrink bytes: modern formats, right dimensions, CDN, good compression.
  4. Cut render-blocking work on the critical path.
  5. Server-render meaningful text when you can — text LCP often beats a late image.
<link rel="preload" as="image" href="/hero.avif" fetchpriority="high" />
<img
  src="/hero.avif"
  width="1200"
  height="630"
  alt=""
  fetchpriority="high"
/>

TTFB still matters: a slow document delays everything downstream. CDN and caching are LCP tools too.

INP — interaction to next paint

Most of a session happens after load. INP watches click / tap / key interactions across the life of the page and reports a high-percentile latency (not “first click only” like FID).

An interaction’s latency is roughly:

input delay → handler work → time until the next paint

Good responsiveness is not “all network finished.” It’s “the user saw a reaction soon” — accordion opens, button depresses, spinner appears.

Thresholds (75th percentile):

  • ≤ 200ms → good
  • ≤ 500ms → needs improvement
  • > 500ms → poor

What actually helps:

  1. Break up long tasks so the main thread can paint and take input (yield with macrotasks / scheduler.yield).
  2. Keep event handlers thin — defer non-critical work.
  3. Avoid forced layout thrash in handlers.
  4. Virtualize huge lists; don’t re-render the world on every keystroke.
  5. Audit third-party scripts that monopolize the main thread.
// Sketch: yield between chunks (prefer platform APIs when available)
async function processAll(items) {
  for (let i = 0; i < items.length; i++) {
    doWork(items[i]);
    if (i % 50 === 0) {
      await new Promise((r) => setTimeout(r, 0));
    }
  }
}

Lab note: INP depends on which interactions you perform. Field data + reproducing the slow interaction beats staring at a load-only Lighthouse run. TBT can be a rough proxy in the lab; it is not INP.

CLS — cumulative layout shift

Unexpected movement of visible content. Classic causes: images without dimensions, late ads/fonts/banners shoved above existing content, dynamic injection.

Fixes:

  1. Always reserve space (width/height or aspect-ratio).
  2. Don’t insert banners above content without a reserved slot.
  3. Font strategy that limits layout swap pain (size-adjust, matched fallbacks, careful font-display).
  4. Prefer transform animations over top/left that reflow.
.card-media {
  aspect-ratio: 16 / 9;
  background: var(--color-bg-muted);
}

Measure like an adult

Field: CrUX (via PageSpeed Insights), your own RUM with the web-vitals library.

import { onLCP, onINP, onCLS } from 'web-vitals';

onLCP(console.log);
onINP(console.log);
onCLS(console.log);

Lab: Lighthouse, WebPageTest, Performance panel — great for why, incomplete for is production good.

Interview framing

Define each metric in one sentence, name the good threshold, give one root cause + one fix, and mention field vs lab. That package beats reciting three numbers alone.

Further reading (web.dev / Chrome)

Related guides