Technical SEO

Core Web Vitals explained by a developer: field data, attribution and root causes

Core Web Vitals beyond the generic tips: CrUX versus lab data, LCP subparts, INP attribution and long tasks, the real causes of CLS and how the work is actually done.

Fabio Folgori6 min read

Close-up of the round dial of a measuring gauge
Photo by Liam Briese on Unsplash

Core Web Vitals are three metrics Google uses to measure the real experience of loading, responsiveness and visual stability: LCP, INP and CLS. The definitions are everywhere. What interests me here is something else: how to read the data correctly, how to trace a bad metric back to its cause, and why so many checklist fixes move nothing.

Field data and lab data

This distinction settles half the arguments.

  • Field data (CrUX): collected by Chrome from real users, aggregated over 28 days, assessed at the 75th percentile. It is what Search Console and Google use. It reflects real devices, networks and behaviour, but it arrives late and only for pages or origins with enough traffic.
  • Lab data (Lighthouse): a simulated load on a fixed device and network. Useful for diagnosing and comparing versions, but it does not measure INP (TBT stands in for it) and does not see what happens after load.

A Lighthouse score of 95 with a field INP of 400 ms is not a contradiction: the lab test never clicked anything. To really understand what is going on you need your own field data, collected with the attribution build of the web-vitals library and sent to an analytics tool.

LCP: break it down before optimising

LCP measures when the largest element in the initial viewport is painted. Saying "LCP is 3.8 seconds" is not enough: it must be broken into four subparts.

  1. TTFB: how long the server takes to send the first byte of HTML.
  2. Resource load delay: time between TTFB and the start of the download of the LCP element's image or font.
  3. Resource load duration: how long that download takes.
  4. Render delay: time between the end of the download and the actual paint.

On a healthy site both "delay" parts are small. In practice, the most frequent problem I find is not a heavy image but a high load delay: the LCP image is a CSS background, is injected by JavaScript, has loading="lazy" or is only discovered once a slider has initialised. The browser finds it late. The fix is to make it discoverable in the initial HTML and give it fetchpriority="high", not to compress it by another 10%.

If TTFB dominates, the problem is on the server or in the architecture: slow dynamic generation, no cache, chained redirects. A site generated in advance and served from a CDN has a TTFB close to the physical minimum.

A high render delay points to CSS or JavaScript blocking paint, or a web font needed by the LCP element arriving late.

INP: a main-thread problem

INP measures the latency of interactions (clicks, taps, keys) across the whole visit and essentially reports the worst. Every interaction has three phases:

  • input delay: the main thread is busy and the event waits;
  • processing time: the event handlers run;
  • presentation delay: the browser recalculates styles and layout and paints the next frame.

The near-universal cause of bad INP is long tasks, blocks of JavaScript over 50 ms that occupy the main thread. Where they come from:

  • third-party scripts (tag managers, chat, multiple analytics, pixels) running during navigation;
  • page builders and frameworks that hydrate the whole page at startup;
  • event handlers doing too much synchronous work, for example filtering an entire list on every keystroke;
  • huge DOMs, where every style and layout recalculation costs tens of milliseconds.

The Long Animation Frames API and web-vitals attribution tell you which script, which function and which element caused the slow interaction. Without attribution you are guessing.

Effective fixes are structural: break up work and yield the thread (scheduler.yield() where available), defer non-urgent work until after the frame is painted, shrink the DOM, load third parties only when needed. A plugin that "delays all scripts" improves lab data and often worsens INP, because it moves execution to exactly the moment of the first interaction.

CLS: shifts with a specific culprit

CLS adds up unexpected layout shifts, grouped into windows. The Performance panel and web-vitals attribution show the shifted element and the one that caused the shift. The real causes, in the order I meet them:

  • content inserted above existing content: cookie banners at the top, notices, ads;
  • images, iframes and videos without dimensions or without aspect-ratio;
  • web fonts with metrics different from the fallback: fixed with size-adjust and related descriptors, not just font-display;
  • animating layout-triggering properties (top, height) instead of transform;
  • components that hydrate and change height after the first paint.

Watch the bfcache too: pages excluded from the back/forward cache reload everything on every "back" and recreate shifts that should not exist.

How I approach the work

  1. Search Console and CrUX to learn which metric, on which device and on which group of pages.
  2. Collect your own field data with attribution, if traffic allows.
  3. Reproduce in the lab with CPU throttling and a Performance panel trace.
  4. Act on the dominant subpart or phase, not on everything at once.
  5. Verify on field data after at least 28 days, because CrUX is a rolling average.

My take

Core Web Vitals reward simple architectures. Static pages served from a CDN start with minimal TTFB, HTML that already contains the LCP element and little JavaScript to run. That is why I build new sites with Nuxt, build-time generation and Tailwind, which ships only the CSS actually used: many of the problems in this article simply never show up.

On WordPress you can reach good values, and I do, but with a multipurpose theme, a builder and dozens of plugins you are fighting the structure. I cover it in Slow WordPress site: a technical analysis and, for anyone weighing a change, in From WordPress to Nuxt, GitHub and Netlify.

Why work with me

I have been programming since 1999, delivered more than 300 projects and work as an external developer with about 30 agencies, from Rome and remotely. I work on Core Web Vitals starting from field data and attribution, not from the score.

If Search Console is flagging problems, get in touch: in a call of about 20 minutes we look at which metric is suffering and why. The service is in Speed and SEO.

Frequently asked questions

The score comes from a lab test on a simulated device, just after load. Search Console uses CrUX data from real users at the 75th percentile, over 28 days and across all interactions. The latter is what counts.

Good LCP within 2.5 seconds, INP within 200 milliseconds, CLS within 0.1, measured at the 75th percentile of visits, separately for mobile and desktop.

They are one of the page experience signals: they matter mostly when content relevance is comparable. Their most tangible effect is on conversions and abandonment, not just ranking.

In the lab, Total Blocking Time is used as a close proxy. Real INP needs field data: CrUX or, better, your own measurement with the web-vitals library sending attribution data to an analytics tool.

  • #core web vitals
  • #technical seo
  • #lcp
  • #inp
  • #cls
  • #performance

More articles

// Contact

A project, a problem or just a question?

Agency, business or professional: choose how you'd rather talk to me. I reply within one working day, no strings attached.