Skip to main content
PerformanceCore Web VitalsPerformanceLCPCLSINPWeb Performance

Core Web Vitals for engineers who don't want to read the spec

LCP, CLS, INP — what they actually measure, why they matter for real users, and the highest-impact fixes for each. No spec-reading required.

7 min read

Google's Core Web Vitals documentation is thorough, technically precise, and absolutely exhausting to read if you just want to know what to actually fix.

This is the version I wish I'd had.

The three metrics that matter

There are three Core Web Vitals. Each one measures a different dimension of the user experience:

  • LCP (Largest Contentful Paint) — how fast does the page look loaded?
  • CLS (Cumulative Layout Shift) — does content jump around while loading?
  • INP (Interaction to Next Paint) — how quickly does the page respond to user input?

Google uses these three metrics as ranking signals. More importantly, they're proxies for whether your website actually feels good to use.

LCP: Largest Contentful Paint

What it measures: The render time of the largest visible element in the viewport — typically a hero image or a large heading.

Good: < 2.5s. Needs improvement: 2.5–4s. Poor: > 4s.

What causes bad LCP

The LCP element is almost always delayed by one of four things:

  1. Slow server response — time to first byte (TTFB) is high
  2. Render-blocking resources — CSS or JavaScript in <head> blocking paint
  3. Slow resource load time — the LCP image is large, unoptimised, or discovered late
  4. Client-side rendering — the LCP element only exists after JavaScript runs

The highest-impact fixes

Fix 1: Preload the LCP image.

If the browser doesn't know the LCP image exists until it's parsed the CSS and discovered an url() reference, you've lost precious seconds. Add a <link rel="preload"> in the <head>:

<link rel="preload" as="image" href="/hero.avif" fetchpriority="high">

Fix 2: Set fetchpriority="high" on the LCP <img> element.

This is a one-attribute change that tells the browser to prioritise this resource over others. It costs nothing and routinely improves LCP by 200–500ms.

<img src="/hero.avif" fetchpriority="high" alt="..." width="1200" height="630">

Fix 3: Serve images in AVIF or WebP.

A JPEG hero image at 800KB becomes 120KB as AVIF. Same visual quality. Faster download, faster LCP.

<picture>
  <source srcset="/hero.avif" type="image/avif">
  <source srcset="/hero.webp" type="image/webp">
  <img src="/hero.jpg" alt="..." width="1200" height="630">
</picture>

Fix 4: Use server-side rendering for content-driven pages.

If your LCP element requires a JavaScript API call to render, move it server-side. The browser should receive a fully-rendered LCP element in the initial HTML payload.


CLS: Cumulative Layout Shift

What it measures: How much visible content unexpectedly moves while the page loads. Measured as a score from 0 to infinity (lower is better).

Good: < 0.1. Needs improvement: 0.1–0.25. Poor: > 0.25.

The most common causes of CLS

Images without explicit dimensions. When a browser encounters an <img> without width and height attributes, it doesn't know how much space to reserve. When the image loads, it pushes content down. This is the single most common CLS cause.

<!-- Bad: browser doesn't know how tall this will be -->
<img src="/product.jpg" alt="Product">

<!-- Good: browser reserves exactly the right space -->
<img src="/product.jpg" alt="Product" width="400" height="300">

Dynamically injected content above existing content. Banners, cookie notices, newsletter popups, and ad slots that appear above existing content cause massive layout shifts. Either reserve space for them before they load, or inject them at the bottom of the document.

Web fonts without font-display: optional or font-display: swap. If a web font loads after text has been rendered in a system font, the text reflows as glyphs change size. Use font-display: swap for body text and font-display: optional to eliminate layout shift entirely (at the cost of occasionally showing a system font on very slow connections).

@font-face {
  font-family: 'MyFont';
  src: url('/fonts/my-font.woff2') format('woff2');
  font-display: swap; /* or 'optional' to eliminate shift entirely */
}

CSS animations that affect layout. Animating height, width, top, left, margin, or padding triggers layout recalculation and contributes to CLS. Animate transform and opacity instead — they're handled on the compositor thread and don't cause layout shifts.

/* Bad: causes layout recalculation and CLS */
.menu { transition: height 0.3s ease; }

/* Good: compositor-thread only, no CLS */
.menu { transition: transform 0.3s ease; }

The CLS fix checklist

  • [ ] Every <img> has explicit width and height attributes
  • [ ] Every <video> has explicit dimensions or uses aspect-ratio CSS
  • [ ] Ad slots and banners have reserved space (min-height or aspect-ratio)
  • [ ] Font loading uses font-display: swap or font-display: optional
  • [ ] No layout-triggering CSS animations above the fold
  • [ ] Skeleton screens maintain the same dimensions as loaded content

INP: Interaction to Next Paint

What it measures: The time from a user interaction (click, tap, keypress) to when the browser actually paints a visual response. Replaced FID (First Input Delay) as a Core Web Vital in 2024.

Good: < 200ms. Needs improvement: 200–500ms. Poor: > 500ms.

INP is the newest and least understood of the three. It's also the one most directly caused by JavaScript.

What causes bad INP

The browser is single-threaded. When JavaScript is running, the browser cannot respond to user input. Long JavaScript tasks block the main thread and delay the visual response to interactions.

Common sources of long tasks:

  • Large event handlers that do too much work synchronously
  • Synchronous rendering triggered by state changes
  • Third-party scripts running during user interactions
  • Unvirtualised lists rendering thousands of DOM nodes

How to diagnose INP issues

Chrome DevTools → Performance panel → record a user interaction → look for long tasks (shown in red, taking > 50ms).

The Long Animation Frames API (available in Chrome 123+) can help you identify which scripts are causing long frames in production:

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) {
      console.log('Long animation frame:', entry);
      // Report to your analytics
    }
  }
});
observer.observe({ type: 'long-animation-frame', buffered: true });

The highest-impact INP fixes

Break up long tasks with scheduler.yield(). The Scheduler API lets you yield control back to the browser mid-task, allowing it to handle user input before resuming:

async function processLargeList(items) {
  for (let i = 0; i < items.length; i++) {
    processItem(items[i]);
    
    // Yield every 50 items to allow browser to handle input
    if (i % 50 === 0) {
      await scheduler.yield();
    }
  }
}

Virtualise long lists. Rendering 1,000 DOM nodes when only 20 are visible is one of the most common sources of poor INP. Use virtual scrolling — @angular/cdk/scrolling in Angular, react-window in React.

Debounce expensive event handlers. Search inputs, resize handlers, and scroll handlers that trigger expensive operations should be debounced:

// Angular example: debounce search input
searchControl.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(query => this.searchService.search(query))
).subscribe(results => this.results = results);

Audit third-party scripts during interactions. Tag managers, chat widgets, and analytics scripts often attach their own event listeners. Use the Performance panel to check whether third-party code is contributing to long tasks during interactions.


Putting it together: a triage framework

When optimising Core Web Vitals, I use this triage order:

  1. Fix CLS first — it's usually the easiest win and doesn't require architectural changes
  2. Fix LCP next — high impact, well-understood fixes
  3. Fix INP last — often requires the most investigation and architectural work

For each metric, use the PageSpeed Insights report to get field data (real user measurements from Chrome UX Report), not just lab data. Lab data tells you what's possible; field data tells you what your actual users are experiencing.

The goal isn't a 100 Lighthouse score. It's real users perceiving your site as fast, stable, and responsive. These three metrics, taken together, are the best proxy we have for that.


Working on Core Web Vitals improvements for a production site? Book a call — I'm happy to look at your specific PageSpeed report and PageSpeed insights data.

DM

Deepak

Full-Stack Engineer based in Germany. I write about performance, Angular, Node.js, design systems, and the craft of building exceptional web experiences.

Book a 15-min call →