awesome-everything RU
↑ Back to the climb

Browser & Frontend Runtime

Metric tradeoffs, RUM attribution, and the CI+field loop

Crux The three vitals share a budget — a fix for one can break another. Complete production observability needs web-vitals RUM for the field verdict and a throttled CI gate for regressions — neither alone is enough.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 16 min

A team fixes LCP by inlining the full critical CSS bundle — LCP render delay drops 400 ms. But TTFB rises 60 ms because the HTML is now 40 KB larger. A separate team lazy-loads every non-hero image to cut the bundle — LCP stays fine, but the hero was accidentally caught and LCP regresses 800 ms. Another team ships a full SPA and gets green vitals on the first load — but every client-side route change is invisible to the metrics. One budget, three metrics, four teams, and no single knob.

The metrics are not independent dials.

Fixing one Core Web Vital can hurt another, and the senior move is to see the system, not individual metrics. Common tradeoffs:

  • Inlining a large critical CSS block helps LCP render delay (no render-blocking request) but bloats the HTML, hurting TTFB. The net effect depends on connection speed and HTML size.
  • Lazy-loading everything to shrink the bundle helps INP (less JS to parse and execute) but, applied accidentally to the hero image, adds a large load delay and wrecks LCP.
  • Reserving generous space for CLS — large min-height containers for ads — can push the LCP element below the fold so it is no longer the LCP candidate. Sometimes that is fine (the new LCP candidate is already fast). Sometimes it is not.
  • Shipping a large interactive framework for a snappy feel adds hydration — one large long task that spikes INP for early interactions. The same feature simultaneously hurts INP and can hurt LCP (JS blocks the render-blocking parse path).

There is no single knob. The discipline: measure all three before and after any change, in the field where possible, in a throttled lab at minimum. A change that improves one metric at the cost of another may or may not be worth it — but you cannot know unless you look at all three.

INP attribution in the field: from metric to line of code.

A field INP number alone is not actionable — it is a latency with no cause attached. The attribution chain:

  1. PerformanceObserver subscribed to event entries gives you, per interaction, the input delay / processing time / presentation delay split.
  2. Long Animation Frames (long-animation-frame entries via PerformanceObserver) give you, for the frame where a slow interaction landed, the array of scripts that ran — with source URLs, function names, and durations.
  3. Combine them: high input-delay INP → find the long task in the LoAF entry; high processing-time INP → the handler, attributed by LoAF; high presentation-delay INP → a heavy render or forced synchronous layout.

The whole point of wiring LoAF into RUM is to turn “p75 INP is 340 ms” into “the search filter function at search.js:88 is the dominant script in slow frames.” That is actionable.

LCP attribution in the field.

The web-vitals library’s onLCP callback delivers not just the LCP time but the element (so you know what the browser picked as LCP), the URL of the resource (if it is an image), and the four-phase breakdown: TTFB, load delay, load time, render delay. Logging these to telemetry means a regression from “LCP went from 1.9 s to 3.8 s” immediately surfaces which phase grew — was it TTFB (a slow deploy), load delay (an accidental loading="lazy"), load time (an unoptimised image), or render delay (a new render-blocking script)?

Attributed field sample from web-vitals RUM
LCP element
img.hero
LCP phase: load time
3510 ms (dominant — 4 MB JPEG)
LCP phase: TTFB + delay + render
610 ms combined
INP
38 ms — good
CLS
0.02 — good

The RUM and CI loop — both halves required.

The complete production observability setup has two halves.

RUM: ship the web-vitals library (or equivalent), which uses PerformanceObserver to capture LCP, INP, and CLS exactly as Chrome scores them, plus attribution (element, phase split, interaction target, shifting nodes). Send them to telemetry tagged by route, device class, country, and release. This is the real verdict — it catches regressions a lab never will, especially device-class regressions and interactions that only real users trigger.

CI: a synthetic gate — Lighthouse CI or a Playwright trace — that runs on every PR, throttled to a realistic mid-tier device, asserting budgets on LCP, total blocking time (the lab proxy for INP), and CLS, and failing the build on a regression.

Neither alone is enough. Lab without RUM ships regressions that only real devices reveal. RUM without a lab gate means every regression reaches production before anyone sees it.

The soft-navigation gap — why SPA vitals need explicit instrumentation.

Core Web Vitals were designed around full page loads. In a single-page app, the first load has a real LCP — but subsequent client-side route changes (“soft navigations”) historically had no LCP measurement at all, because no document load event fires. To a user a soft navigation feels exactly like a page load — they clicked a link and expect new content fast — but the metric did not see it.

Chrome has been shipping soft-navigation support to attribute LCP and other vitals to client-side route changes, but coverage is still maturing and not all frameworks emit the right hints. The consequence: for a SPA, do not assume your vitals story is complete just because the initial-load numbers are green. Snappy first load + sluggish route transitions is a real, common, and historically under-measured failure. Instrument soft navigations explicitly with the PerformanceObserver soft-navigation entries or your own RUM marks on router.beforeEach / route change events.

Why this works

The CLS session window (worst 5-second cluster rather than lifetime sum) is one concrete example of the spec evolving to match real user experience rather than pure engineering measurement. The original lifetime-sum CLS unfairly penalised long-lived pages and infinite scroll — a shift that happened four minutes into a session counted the same as one that happened at second 2. The session window focuses the metric on concentrated bad behavior: a burst of shifts during ad reload or a batch of unsized images loading. It is more representative of what a user actually notices in context, and it changed how you reason about CLS: spreading shifts apart across windows can reduce the score even without fixing the root cause, but the root cause (unreserved content) still creates a bad experience.

Debug this
log
[LCP] value: 4120 ms  rating: poor
element: img.hero
url: /assets/hero-original.jpg  (3.8 MB, JPEG, 4000x3000)
phase split: ttfb 280ms | loadDelay 90ms | loadTime 3510ms | renderDelay 240ms

[INP] value: 38 ms  rating: good
[CLS] value: 0.02  rating: good

Read the phase split and the element details — which phase dominates, what is the root cause, and what is the fix? What should the team NOT do, and why?

Which RFC?

Which browser API is the basis for measuring LCP, INP, layout shifts, and Long Animation Frames in real-user monitoring?

Design challenge

Design the Core Web Vitals strategy for a media site: article pages with a hero image, ads, embeds, and a comments section. Hit good LCP, INP, and CLS at p75 in the field, and keep them green over time.

  • Hero image is the LCP element on every article.
  • Ads and social embeds load late and into the article body.
  • Comments section is interactive and below the fold.
  • The site is server-rendered and hydrates.
  • Targets: LCP ≤2.5 s, INP ≤200 ms, CLS ≤0.1 — all at field p75.
  • Regressions must be caught before they reach production.
Quiz

Why can a page show a great Lighthouse score but still be flagged for poor Core Web Vitals in Search Console?

Recall before you leave
  1. 01
    A page has poor LCP. Walk through how you diagnose it using the phase split and why reading the phase split is the key step.
  2. 02
    Explain the complete production observability setup for Core Web Vitals — what RUM provides, what CI provides, and why neither is sufficient alone.
  3. 03
    Why are INP and the SSR/hydration material inseparable, and what does an early-only INP pattern tell you?
Recap

The three Core Web Vitals share a performance budget: inlining CSS helps LCP render delay but hurts TTFB; lazy-loading reduces bundle size but can add LCP load delay if accidentally applied to the hero; generous CLS space reservation can change the LCP candidate. Every change must be measured against all three metrics, not one in isolation. Complete production observability requires both RUM (web-vitals library + PerformanceObserver, sending attributed LCP/INP/CLS to telemetry tagged by route and device class) and a throttled CI gate (Lighthouse CI or Playwright, budgeting LCP/TBT/CLS per PR). RUM is the real verdict; CI catches regressions before they reach production. INP in SPAs is further complicated by soft navigations, which historically had no LCP attribution — instrument route changes explicitly. The field p75 from CrUX is the only number that determines ranking; a lab fix that does not move it did not help real users.

Connected lessons
appears again in267
Continue the climb ↑Core Web Vitals: multiple-choice review
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.