Diego Betto's Blog
Photo by Marc-Olivier Jodoin on Unsplash

August 28, 2026 · 6 min di lettura

Core Web Vitals: LCP, CLS and INP explained with a real case

What LCP, CLS and INP actually measure, why your local Lighthouse often lies, and the real fixes applied on this blog to get past it.

Condividi:XLinkedInFacebookWhatsApp

A few weeks ago I ran Lighthouse on this very blog after some changes, expecting a clean 100 in Performance. I got 95. Total Blocking Time at 182ms, a report full of “unused” JavaScript I’d never written myself, and a console error caused — literally — by a browser extension that analyzes search engines. None of those three things had anything to do with the site’s code.

This article covers what I dug into: what LCP, CLS and INP actually measure, when the number you’re seeing is noise rather than a real problem, and the three genuine fixes I applied here — with numbers, not just a claim.

The three metrics, without the jargon

LCP (Largest Contentful Paint) measures how long it takes for the largest visible element in the initial viewport to appear — almost always a hero image or a big block of text. Under 2.5 seconds is “good”.

CLS (Cumulative Layout Shift) measures how much the page “jumps” while loading — an image without width/height that pushes the text below it down is the textbook example. Under 0.1 is “good”.

INP (Interaction to Next Paint) measures the time between a user’s click/tap and the moment the browser visually updates the screen in response. Under 200ms is “good”.

ℹ️ Nota

INP replaced FID in March 2024: if you’ve read older guides, they probably talk about FID (First Input Delay) as the third metric. Google officially replaced it with INP because FID only measured the delay before the interaction started being processed — INP measures the whole experience, all the way to the rendered frame. A site can have great FID and terrible INP if the interaction starts right away but the work that follows blocks the main thread for a long time.

The problem (almost) no guide tells you: your local Lighthouse lies

Here’s what I found digging into the raw JSON of that 95 report: dozens of chrome-extension:// entries in the unused-JavaScript audits. A password manager. React DevTools. An ad blocker with its EasyList/EasyPrivacy filters loaded. All running during the measurement, all counted in Total Blocking Time as if they were the site’s own code.

The console error that failed the Best Practices audit? net::ERR_BLOCKED_BY_CLIENT on a Cloudflare analytics script — blocked by my own ad blocker, not a site bug.

⚠ Attenzione

Don’t trust a Lighthouse audit run from a regular Chrome profile: if you have extensions installed — and anyone who develops always has at least five — what Lighthouse measures includes their code running on the page. For a reliable number: use an incognito window with extensions disabled, or

PageSpeed Insights

, which runs in a controlled environment away from your browser.

There’s also a deeper distinction to keep in mind, beyond extensions: lab data isn’t field data. Lighthouse (lab data) runs a single page loaded once, under simulated conditions. The real Core Web Vitals Google uses for ranking comes from CrUX (Chrome UX Report) — aggregated data from real users, over a rolling 28-day window, visible in Search Console. A site can have a perfect local Lighthouse and a mediocre field INP if, say, most of its real traffic comes from low-end smartphones that your dev laptop doesn’t simulate.

Three real fixes, applied here

Once the noise was stripped out, three genuinely mine problems remained. I’m listing them with before/after because theory without numbers convinces no one.

1. A CSS file blocking render

The site’s global stylesheet (~10KB compressed, shared by every page) was a render-blocking request: the browser had to download and parse it before the first paint. With Astro, the fix was one config line:

// astro.config.mjs
export default defineConfig({
  build: {
    inlineStylesheets: "always",
  },
});

With inlineStylesheets: 'always', the CSS ends up directly inside the <head> of every page instead of an external file to request. The trade-off is real — you lose the shared cache of the file across pages — but for a blog where most visitors arrive from a search on a single article (not browsing ten pages in the same session), removing that blocking request matters more than the cache.

2. The LCP image loaded as if it didn’t count

The homepage’s first three cards — the ones always visible without scrolling — used loading="lazy" on the cover, the same directive used for every image further down the page. For the element that is the LCP, lazy loading is counterproductive: it tells the browser to wait before it even starts discovering that resource.

<Image
  src={cover}
  alt={coverAlt}
  loading={isAboveTheFold ? 'eager' : 'lazy'}
  fetchpriority={isFirstCard ? 'high' : undefined}
  width={400}
  height={400}
/>

loading="eager" on the above-the-fold cards, and fetchpriority="high" only on the very first one — the one statistically most likely to be the actual LCP element. No need to max-priority everything: just that one.

3. Images heavier than they needed to be

The same cards served a fixed 400×400px raster regardless of how big they actually were on screen — on certain grid-layout breakpoints, they were shown at a real 250×250px. Bytes downloaded for pixels never displayed.

<Image
  src={cover}
  widths={[260, 400, 600]}
  sizes="(min-width: 768px) 260px, (min-width: 640px) 45vw, calc(100vw - 2rem)"
  ...
/>

With widths + sizes, Astro generates a real srcset and the browser picks the right variant based on the layout’s actual width, not the largest one available regardless.

💬 Opinione personale

Chasing the exact 100 is almost always wasted time: after these three fixes, what was left were fractions of a point — FCP and LCP already under a second, a hair away from the max score because of Lighthouse’s scoring curve, not because of a real problem to solve. At some point it’s worth stopping and looking at the real field data (Search Console → Core Web Vitals) instead of endlessly optimizing a lab number your users will never see.

What to take away

If you’re looking at a low Lighthouse report and don’t know where to start: first separate the noise (extensions, third-party scripts blocked by your own browser) from the signal, then look specifically at the LCP element (is it loading lazy? does it have fetchpriority?) and the above-the-fold images (are they sized for the real layout or just for the worst case?). Those are the two spots where, empirically, most of the fixable-in-an-afternoon problems hide.

If your field INP stays high even after fixing LCP and CLS, the next suspect is JavaScript blocking the main thread on frequent interactions — I’ve written separately about how debounce and throttle can stop a single event (scroll, resize, typing) from triggering repeated, unnecessary work. And if you use Astro, it’s also worth checking small things like trailing slash handling, which look like routing details at first glance but can generate avoidable redirects — another couple of milliseconds lost before the page even really starts loading.

Condividi:XLinkedInFacebookWhatsApp