Web Development Tip Featured

Core Web Vitals in practice

Core Web Vitals are three numbers that describe how a page feels to a real person: how fast the main content appears, how quickly the page responds to input, and how much it moves while someone is reading. Google has used them as a ranking signal since 2021, but the ranking effect is modest. The honest reason to care is that a page failing all three feels broken, and people leave.

What each metric actually measures

  • LCP — Largest Contentful Paint. The render time of the largest image or text block in the initial viewport. Good is 2.5 seconds or less.
  • INP — Interaction to Next Paint. The latency from an interaction to the next painted frame that shows a response. Good is 200 milliseconds or less. INP replaced First Input Delay in March 2024, because FID only measured the delay before a handler started, not the work the handler did.
  • CLS — Cumulative Layout Shift. A unitless score for unexpected movement of visible content. Good is 0.1 or less.

All three are judged at the 75th percentile of real page views, not the average. A page that is instant for nine out of ten visitors and unusable for the tenth still fails.

Advertisement

LCP: usually the hero image, not the server

LCP splits into four phases: time to first byte, resource load delay, resource load duration, and element render delay. Most failing LCP scores are frontend problems in the last two.

The usual culprits:

  • A hero image discovered late because it is defined in CSS as a background-image, or injected by JavaScript after hydration.
  • Render-blocking stylesheets and synchronous scripts in the document head.
  • An oversized asset: a 3000 px JPEG rendered into a 400 px slot.
  • Too many preload hints, which push the real LCP resource down the queue.
<img src="/hero-800.avif"
     srcset="/hero-800.avif 800w, /hero-1600.avif 1600w"
     sizes="(max-width: 840px) 100vw, 800px"
     width="800" height="450"
     fetchpriority="high" decoding="async"
     alt="Dashboard overview">

fetchpriority="high" promotes the image in the preload scanner's queue. Use it on exactly one image per page — the LCP candidate. Marking everything high is identical to marking nothing. Never combine it with loading="lazy": lazy-loading the hero is the most common self-inflicted LCP regression on the web.

Serve modern formats too. AVIF typically lands 40–60% smaller than JPEG at the same perceived quality, and WebP around 25–35% smaller. If the LCP element is a text block instead of an image, the bottleneck is almost always the webfont, so preload it and set font-display: swap.

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-var.woff2") format("woff2");
  font-display: swap;
}

INP: long tasks on the main thread

Browsers paint and run JavaScript on a single thread per origin. If one task occupies that thread for 300 ms, every tap and keystroke waits behind it. INP is roughly input delay plus handler time plus presentation delay, reported from the worst interactions in a session.

Common causes: a 400 ms third-party analytics bundle, a synchronous localStorage read on every keystroke, rendering 5000 table rows as someone types, or a hydration pass that blocks a mid-range Android phone for a full second.

  • Break long tasks apart: await scheduler.yield() where supported, otherwise setTimeout(..., 0) inside the loop.
  • Debounce expensive work. Filtering on every keystroke is rarely required; 150 ms of debounce is imperceptible.
  • Virtualise long lists so you only render what is on screen.
  • Move heavy parsing, sorting, or crypto into a Web Worker.
  • Audit third-party tags and load them after the first interaction or on idle.

content-visibility: auto is a cheap win for long pages: offscreen sections skip rendering entirely. Pair it with contain-intrinsic-size so the browser reserves space and CLS does not spike.

.article-section {
  content-visibility: auto;
  contain-intrinsic-size: auto 600px;
}

CLS: dimensions, fonts, and late-injected ads

Each shift is scored as impact fraction multiplied by distance fraction, so a small element moving a long way hurts as much as a large one moving slightly.

  • Always set width and height on <img> so the browser derives an aspect ratio and reserves the space. Add height: auto in CSS so it still scales.
  • Reserve space for ad slots, embeds, and cookie banners with a min-height, and never insert a banner above already-rendered content.
  • Use font-display: optional for non-essential display faces. It removes the swap-induced shift entirely.
  • Animate with transform, never with top or height.
Shifts that occur within 500 ms of a user interaction are excluded from CLS. That is not permission to shift; it means a badly timed shift may simply go uncounted while still annoying people.

Measure with field data first

  • PageSpeed Insights shows both: CrUX field data for the URL or origin, then a Lighthouse lab run. Read the field numbers first.
  • Chrome UX Report (CrUX) is 28-day rolling field data, available through the CrUX API and BigQuery. It is the reference for how your origin actually performs.
  • Lighthouse is a controlled lab test. Excellent as a regression gate in CI, misleading as a proxy for your users' devices.
  • Real-user monitoring is the only way to see your own 75th percentile. The web-vitals library reports all three metrics in a few lines, and the DevTools Performance panel explains why an interaction was slow.
import { onLCP, onINP, onCLS } from "web-vitals";
const send = (metric) =>
  navigator.sendBeacon("/rum", JSON.stringify(metric));
onLCP(send);
onINP(send);
onCLS(send);

Segment that data by device and connection. A site that passes on desktop fibre and fails on a mid-range Android over 4G has a mobile problem, and the fix is almost always less JavaScript.

The highest-leverage fixes, in order

  1. Cut JavaScript. Fewer bytes and fewer long tasks improve INP, LCP, and TBT at once.
  2. Fix the LCP element. Correct size, modern format, fetchpriority="high", never lazy.
  3. Reserve space for images, ads, and embeds, and handle fonts deliberately.
  4. Defer third parties and measure their real cost instead of assuming it is small.
  5. Trim render-blocking CSS to the above-the-fold rules and inline that subset.

A short shipping checklist

  • Every <img> has width, height, and alt text.
  • Exactly one fetchpriority="high" per page, and the hero is never lazy.
  • Fonts use font-display: swap or font-display: optional, and the critical face is preloaded.
  • No third-party script runs before first paint unless it is provably required.
  • Field monitoring is wired up before launch, not after the first complaint.

If you have no traffic yet and cannot install real-user monitoring, stay in the lab and optimise the single largest blocking resource. Just do not claim a passing score means a fast site until real users agree with it.

Advertisement
khallaf

Writing about programming, AI and the tools that make engineering teams faster. Published by A1 Systems.

Last updated 19 Sep 2026

// Keep reading

Related articles

Tools & Tricks 5 min read

Regex you will actually use

The small set of regex constructs that cover everyday work, the patterns worth keeping in a snippet file, and how to avoid catastrophic backtracking.

khallaf Tip