---
title: "Core Web Vitals: what actually moves the needle."
date: 2024-02-15
url: https://remiam.co.uk/notes/core-web-vitals-what-moves-the-needle
tags: [Performance, Web Vitals, SEO]
read_time_minutes: 9
description: "Core Web Vitals — LCP, CLS, INP — explained for engineers and product owners. Real-world fixes that move the score, with thresholds, examples, and references."
---

# Core Web Vitals: what actually moves the needle.

*Published 2024-02-15 · 9 min read · by Liam (Remiam)*

LCP, CLS, INP — three letters that decide whether your site ranks. Here is what they really measure, what fixes them in practice, the thresholds Google uses, and the audit we run on every production site every quarter.

Core Web Vitals are real. They affect ranking, they affect conversion, and they catch teams off guard because the lab scores look fine and the field scores are catastrophic. The lab is on a beefy laptop on a fast connection. The field is on a mid-range Android on a flaky train. Treat the field data as the truth and the lab data as the hypothesis.

## The thresholds Google uses

| Metric | Good | Needs improvement | Poor |
| --- | --- | --- | --- |
| LCP (Largest Contentful Paint) | ≤ 2.5s | > 2.5s, ≤ 4s | > 4s |
| INP (Interaction to Next Paint) | ≤ 200ms | > 200ms, ≤ 500ms | > 500ms |
| CLS (Cumulative Layout Shift) | ≤ 0.1 | > 0.1, ≤ 0.25 | > 0.25 |
| FCP (First Contentful Paint, supporting) | ≤ 1.8s | > 1.8s, ≤ 3s | > 3s |
| TTFB (Time to First Byte, supporting) | ≤ 800ms | > 800ms, ≤ 1.8s | > 1.8s |

*Core Web Vitals thresholds — 75th percentile of real user data.*

## LCP — Largest Contentful Paint

The time from navigation start until the largest above-the-fold element appears. Almost always a hero image, a font-driven headline, or a video poster. This is the metric that correlates most strongly with the 'does this site feel fast?' perception users have.

- Find the LCP element with PageSpeed Insights — it tells you exactly which element it is.
- Preload it with <link rel="preload" as="image" href="...">.
- Compress it — WebP or AVIF over JPEG, with sizes hints for responsive variants.
- Serve it from a CDN that is actually fast in your market — Cloudflare, Fastly, or a regional equivalent.
- Stop blocking it behind a 200KB hydration bundle — defer your JS, render the LCP element in SSR.
- Use fetchpriority="high" on the LCP image to hint the browser.

## CLS — Cumulative Layout Shift

The total of all unexpected layout shifts during page life. Caused by content jumping around as images load, fonts swap, ads inject. The single most-overlooked metric — easy to fix once you know what to look for.

- Set explicit width and height on every image. The aspect-ratio CSS property is a clean alternative when dimensions are dynamic.
- Reserve space for ads, embeds, and third-party widgets before they render — use min-height or aspect-ratio.
- Never inject content above the fold after first paint — banners that drop down 200ms after load are the worst offenders.
- Use font-display: swap or font-display: optional with a metric-compatible fallback to minimise font-swap shift.
- Be careful with skeleton loaders — they should match the dimensions of what they replace.

## INP — Interaction to Next Paint

The time from a user interaction (click, tap, key press) to the next visual update. Replaced FID in March 2024 as the official responsiveness metric — and it's much more sensitive. INP failures are common on JS-heavy sites that felt fine in lab tests.

- Break up long tasks. If a handler blocks the main thread for 200ms, that's your problem.
- Move heavy work into web workers when you can.
- Throttle / debounce inputs — typeaheads are the worst offenders.
- Use requestIdleCallback for non-critical work.
- Audit third-party scripts. The chat widget you added in 2020 is probably your worst INP offender.
- Inspect long tasks in DevTools Performance panel — find the 200ms+ blocks and refactor them.

## Measuring INP locally

```typescript utils/web-vitals.ts
import { onINP, onLCP, onCLS } from 'web-vitals'

// Report each metric to your analytics endpoint
onLCP(({ value }) => analytics.track('lcp', { value }))
onINP(({ value }) => analytics.track('inp', { value }))
onCLS(({ value }) => analytics.track('cls', { value }))
```

> If the field data in Search Console disagrees with the lab data in Lighthouse, trust the field data. The lab is a hypothesis. The field is the truth.

## The audit we run quarterly

- Pull 28-day field data from Search Console for every active client property.
- Triage URLs by impact — 'poor' on a high-traffic page is the priority.
- Run PageSpeed Insights on the worst offenders to surface specific fixes.
- Identify regressions vs the previous quarter. CWV scores drift over time as third-party scripts accumulate.
- Ship fixes in batches. Each major intervention earns a single before/after CrUX comparison.

Core Web Vitals are not optional. They affect ranking, they predict conversion, and the audit is cheap. Every production project we ship has a quarterly review where we look at the field data and pick the next intervention. Sites that don't do this drift downwards every quarter; sites that do, stay green.

## References

1. [Web.dev — Core Web Vitals](https://web.dev/articles/vitals)
2. [Chrome User Experience Report (CrUX)](https://developer.chrome.com/docs/crux/)
3. [PageSpeed Insights](https://pagespeed.web.dev/)
4. [web-vitals JavaScript library](https://github.com/GoogleChrome/web-vitals)
