Engineering
What Core Web Vitals Actually Cost You in Revenue
A small ranking factor and a large conversion factor. What LCP, CLS and INP measure, what causes bad scores, and how to fix them in order.
Core Web Vitals get discussed as an SEO topic. That framing undersells them, because as a ranking signal they are genuinely minor — Google has said as much, and a slow page with the best answer still outranks a fast page with a worse one.
The reason to care is conversion. A visitor who leaves before your page renders converts at zero, and no amount of ranking fixes that.
Here is what the three metrics measure, what actually causes bad numbers, and what to fix first.
The three metrics
Largest Contentful Paint (LCP)
When the largest visible element finishes rendering. Usually the hero image, the H1, or a large text block.
- Good: under 2.5 seconds
- Needs improvement: 2.5–4.0 seconds
- Poor: over 4.0 seconds
LCP is the closest proxy for "when did this page become useful." It is the one to fix first.
Cumulative Layout Shift (CLS)
How much content jumps around while loading. You have experienced this: you go to tap a link, an ad loads above it, the page shifts, and you tap something else.
- Good: under 0.1
- Poor: over 0.25
CLS is usually the cheapest of the three to fix, because the causes are few and well understood.
Interaction to Next Paint (INP)
How long the page takes to visually respond after a user interacts. It replaced First Input Delay in March 2024, and it is stricter: it measures every interaction across the visit, not just the first.
- Good: under 200ms
- Poor: over 500ms
INP is almost always a JavaScript problem — the main thread is busy and cannot repaint.
Lab data versus field data
This distinction causes a great deal of confusion, so it is worth being precise.
Lab data is a simulated load in a controlled environment — what Lighthouse in Chrome DevTools gives you. It is reproducible and useful for debugging, and it is not what Google uses for ranking.
Field data comes from the Chrome UX Report: real measurements from real Chrome users over a rolling 28-day window. This is what Google actually uses, and it is what appears in Search Console.
They routinely disagree, and when they do, field data is the truth. A site can score 95 in Lighthouse on a developer's fast machine and fail field data because most real visitors are on mid-range Android phones on congested mobile networks.
Practical implication: optimise against field data, debug with lab data. And note that field data lags — a fix takes up to 28 days to fully appear.
Our free SEO audit tool reports both, pulled from Google's own PageSpeed Insights API, so you can see where they diverge for your domain.
What actually causes bad LCP
In rough order of how often it is the culprit:
Images
The LCP element is an image on most pages, and it is usually far larger than it needs to be.
The failure pattern is consistent: a 700KB JPEG at 3000px wide, displayed at 640px, loaded with no priority hint and no modern format alternative.
The fix:
<picture>
<source srcset="/hero-640.avif 640w, /hero-1280.avif 1280w" type="image/avif" />
<source srcset="/hero-640.webp 640w, /hero-1280.webp 1280w" type="image/webp" />
<img src="/hero-640.jpg" alt="…"
width="640" height="480"
fetchpriority="high" decoding="async" />
</picture>
Four things are happening there. AVIF and WebP cut file size by 50–80% against JPEG at equivalent quality. srcset sends phones a phone-sized file. width and height reserve the space, which also fixes CLS. And fetchpriority="high" tells the browser this image matters more than everything else it is fetching.
That last attribute is frequently the single highest-return change on a page, and it costs nothing.
Do not put loading="lazy" on your LCP image. Lazy loading defers the fetch, which is precisely wrong for the element you are being measured on. Lazy-load everything below the fold; eager-load the hero.
Render-blocking resources
Every stylesheet in the head blocks rendering until it downloads and parses. So does every synchronous script.
The classic mistake is a chained request:
/* in styles.css — creates a chain */
@import url('https://fonts.googleapis.com/css2?family=…');
The browser must download styles.css, parse it, discover the import, download the font CSS, parse that, then download the font files. Four sequential round trips before text renders.
Put the font stylesheet in the HTML head directly, with preconnect ahead of it:
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=…&display=swap" />
display=swap matters: it renders text immediately in a fallback font rather than leaving a blank space while the webfont loads.
Server response time
If Time To First Byte exceeds 600ms, nothing downstream can be fast. Causes are unexciting and fixable: no caching layer, an under-provisioned host, a slow database query on every page load, or a CDN that is not actually in front of the origin.
For a marketing site the answer is usually static generation. If the page does not change per visitor, it should be a file on disk, not a page assembled per request.
Too much JavaScript
A framework that ships 400KB of JavaScript to render a page of text is paying a large cost for nothing. On a mid-range Android phone, parsing and executing that much JavaScript can take over a second before anything appears.
This is not an argument against frameworks — it is an argument against using a client-rendered framework for content that could be static HTML.
What actually causes bad CLS
Three causes cover nearly all cases.
Images without dimensions. Without width and height, the browser cannot reserve space, so everything below jumps when the image arrives. Always set both — with height: auto in CSS so the image stays responsive.
Webfonts swapping. The fallback font renders, then the webfont loads with different metrics, and the text reflows. Mitigate with font-display: swap plus size-adjust on a @font-face fallback, or by using a fallback with similar metrics.
Content injected above existing content. Cookie banners, promo bars, ads, late-loading embeds. Anything inserted at the top of the document pushes everything down. Reserve the space, or position it as an overlay rather than in flow.
What actually causes bad INP
INP is main-thread contention. Common sources:
- Long tasks — any single JavaScript task over 50ms blocks response
- Heavy event handlers — expensive work directly in a click or input handler
- Third-party tags — analytics, chat widgets, heatmaps, A/B testing scripts all compete for the main thread
- Excessive re-rendering — a framework re-rendering a large tree on every keystroke
Fixes, in order of leverage:
- Audit third-party scripts. Most sites carry tags nobody remembers adding. Remove what is not earning its cost, and load the rest with
deferor after interaction. - Break up long tasks. Yield to the main thread with
scheduler.yield()where available, orsetTimeout(…, 0). - Debounce expensive handlers. Especially on
inputandscroll. - Move heavy computation to a Web Worker.
What to fix first
Given limited time, in this order:
- Compress and correctly size the LCP image, add
fetchpriority="high". Usually the largest single improvement, often an hour of work. - Add
width/heightto every image. Fixes most CLS. Trivial. - Remove render-blocking
@importchains. Move fonts to the head withpreconnect. - Audit third-party scripts and remove what is unused. Helps INP and LCP simultaneously.
- Lazy-load below-the-fold images. Not the hero.
- Address TTFB if it is above 600ms. Bigger job, but nothing else works without it.
An honest note on scores
Chasing a perfect 100 in Lighthouse is usually a bad use of time. The difference between 85 and 100 rarely changes user behaviour; the difference between 35 and 75 changes it substantially.
Get into the "good" thresholds on field data for all three metrics, then stop and go work on your conversion path. Performance is a means to revenue, not an end in itself — and a fast page with a bad offer still does not convert.