Skip to content
NEWAI search optimizationSee how

Performance

Core Web Vitals: what actually moves LCP, INP and CLS

A practitioner's guide to the three Core Web Vitals: the real thresholds, why Lighthouse disagrees with Search Console, and the fixes that move field data.

Bizup LLC

Seattle web design, development and SEO studio

7 min read

Speed optimization

Most Core Web Vitals work fails for the same reason: the team optimises the number they can see on their own laptop, not the number Google is actually looking at. Those are two different measurements, taken from two different populations, and they routinely disagree.

Here is what the three metrics measure, where the thresholds sit, and — for each one — the handful of changes that reliably move real-user data rather than a lab score.

The three metrics and their thresholds

  • Largest Contentful Paint (LCP) — when the biggest above-the-fold element finishes rendering. Good is ≤ 2.5s, poor is > 4.0s.
  • Interaction to Next Paint (INP) — the latency of a page's interactions, reported close to the worst one. Good is ≤ 200ms, poor is > 500ms. It replaced First Input Delay as a Core Web Vital in March 2024, and it is much harder to pass, because FID only measured input delay while INP measures the whole round trip to the next frame.
  • Cumulative Layout Shift (CLS) — how much visible content moves without the user causing it. Good is ≤ 0.1, poor is > 0.25.

All three are assessed at the 75th percentile of page loads, segmented by mobile and desktop, over a rolling 28-day window. That has two consequences people miss. First, your median user's experience is irrelevant — the slowest quarter sets your score. Second, a fix you shipped last week will not show up in full for a month.

Field data versus lab data

Lighthouse and PageSpeed Insights' "performance score" are lab measurements: one simulated load, on a throttled connection, in a clean browser, with no extensions and no cache. Search Console and the Chrome User Experience Report are field measurements: what actually happened to real Chrome users on real devices.

Lighthouse cannot measure INP at all, because there is no user to interact with the page. It substitutes Total Blocking Time, which correlates with INP but is not the same thing. A site can score 100 in Lighthouse and still fail INP in the field — this is common on sites with heavy client-side interactivity that the lab run never triggers.

LCP: break it into four parts before touching anything

LCP is not one thing. It decomposes into four sub-parts that add up to the total, and the fix is completely different depending on which one dominates:

  1. Time to first byte — server and network latency before any HTML arrives.
  2. Resource load delay — the gap between the first byte and the browser starting to fetch the LCP resource.
  3. Resource load duration — how long that fetch takes.
  4. Element render delay — the gap between the resource finishing and the pixels appearing.

On most sites we audit, the dominant sub-part is load delay — the browser knows the HTML is there but has not discovered the hero image yet, usually because it is injected by JavaScript, set as a CSS background, or lazy-loaded. Lazy-loading the LCP image is the single most common self-inflicted performance wound on the web.

html

<!-- Discoverable in the initial HTML, eagerly loaded, prioritised. -->
<img
  src="/hero-960.avif"
  srcset="/hero-640.avif 640w, /hero-960.avif 960w, /hero-1440.avif 1440w"
  sizes="(max-width: 768px) 100vw, 640px"
  width="960" height="640"
  fetchpriority="high"
  decoding="async"
  alt="…"
>

<!-- If the image is on another origin, pay the handshake early. -->
<link rel="preconnect" href="https://images.example-cdn.com" crossorigin>
In Next.js, the priority prop on next/image emits the preload and fetchpriority for you.

If render delay dominates instead, the resource arrived on time and something is blocking paint — render-blocking CSS, a font that is still swapping, or a framework that will not paint until hydration finishes. If TTFB dominates, no front-end change will save you; that is caching, origin configuration or a slow database query.

INP: it is almost always your own JavaScript

An interaction has three phases — input delay, processing, and presentation. INP is the sum. Failing INP nearly always means the main thread was busy when the user tapped, or the event handler itself did too much before yielding.

The reliable fixes, in the order they usually pay off:

  • Break up long tasks. Anything over 50ms on the main thread is a window where input cannot be handled. Chunk the work and yield between chunks.
  • Yield before doing expensive work in a handler. Update the UI first so the user sees a response, then do the rest after a yield.
  • Cut hydration. Server components, islands, or simply not shipping a framework for a page that is mostly text. The cheapest interaction is the one that needed no JavaScript — a native <details> element has an INP of effectively zero.
  • Audit third parties. Chat widgets, tag managers, heatmap recorders and consent banners are usually the top three long-task sources. Load them after interaction or on idle, not in the head.
  • Shrink the DOM. Style and layout recalculation scales with node count. content-visibility: auto on long off-screen sections helps when the page is genuinely large.

js

// Paint the visible change, then yield before the expensive part.
button.addEventListener("click", async () => {
  setExpandedState(true);              // cheap, visible, immediate

  await scheduler.yield();             // let the browser paint + handle input

  const rows = await buildLargeTable(); // the costly work, off the critical path
  render(rows);
});
scheduler.yield() is Chromium-only today — a setTimeout(…, 0) fallback behaves acceptably elsewhere.

CLS: reserve the space, every time

CLS is the most fixable of the three and the one most often left broken, because it is invisible to anyone testing on a warm cache and a fast connection. Four causes account for nearly all of it:

  • Images and video without dimensions. Always set width and height attributes (or an aspect-ratio). The browser then reserves the box before the file arrives.
  • Ads, embeds and iframes. Reserve a min-height for the slot. If the slot can collapse, let it collapse to the reserved size rather than to zero.
  • Content injected above existing content. Cookie bars, promo banners and "you have unsaved changes" strips. Render them in the initial HTML or overlay them — never push the page down after paint.
  • Font swap. When the web font has different metrics from the fallback, every line reflows on swap. Fix it with size-adjust, ascent-override and descent-override on the @font-face fallback. next/font calculates these automatically, which is most of why it exists.

The order we work in

  1. Pull 28 days of field data and find which metric actually fails, on which device class, on which template.
  2. Reproduce it in the lab and identify the dominant sub-part — not the symptom.
  3. Fix the single biggest contributor, deploy, and leave it alone.
  4. Wait for the field window to move. Repeat.

That last step is where most engagements go wrong. Teams ship eleven changes in one release, the number moves, and nobody knows which change did it — so nobody learns anything transferable to the next site.

What good looks like

Across the sites we ship, average LCP sits at 1.4 seconds and Lighthouse performance stays at 95 or above. Those are our build standards, not a one-off demo page: it is far cheaper to hold a performance budget from the first commit than to recover one after launch.

If you already have the site and the numbers are bad, the retrofit is still worth doing — it is just a different job, and it starts with the field data rather than the design. That is what our speed optimization work is.

Keep reading

More onPerformance

All insights
Performance

6 min read

Images are usually the whole problem

Most slow pages are slow because of images. The formats worth using, how to size them correctly, when lazy loading backfires, and the five-minute audit.

Read the article
Performance

7 min read

The real cost of a page builder, measured

What a page-builder site actually ships: a megabyte of CSS across dozens of files, duplicated JavaScript runtimes, and a DOM deep enough to slow interaction.

Read the article