The three metrics Google grades you on: loading, responsiveness, visual stability. Their thresholds, what causes each, the fixes, and why it's field data at the 75th percentile — not your laptop.
Each maps to one thing a user feels. Memorize the metric, the threshold, and the one-word feeling.
Time until the largest element in the viewport (usually the hero image or H1 block) is painted. “Did the main content show up fast?”
Worst-ish latency from a tap/click/keypress to the next frame, across the whole visit. “When I interact, does it respond?”
How much visible content jumps around unexpectedly (unitless score). “Did the page move under my thumb?”
Between good and poor is “needs improvement.” You pass a metric when ≥ 75% of real visits hit the “good” threshold. [web.dev]
“Three vitals, three feelings: LCP = did it load, INP = does it react, CLS = does it hold still. Good = 2.5s / 200ms / 0.1, judged at the 75th percentile of real users.”
Core Web Vitals are scored from field data (real users, a.k.a. RUM / CrUX) — not your fast laptop on office wifi (lab data, e.g. Lighthouse). And it's the 75th percentile: you're graded by your slower quarter of visits, not the median.
“Lab tells you why; field tells you whether. I debug in Lighthouse but I'm graded on p75 field data — so I instrument RUM and watch the slow quarter, not my own machine.”
The three vitals don't stand alone — they sit on a timeline of diagnostic metrics. Google doesn't grade these, but they tell you where on the path a vital is breaking, so you can localize a regression instead of guessing.
| Metric | What it is | Good | Role |
|---|---|---|---|
| TTFB Time to First Byte |
request start → first byte of the HTML (server + network) | ≤ 0.8s | Precursor to everything — a slow TTFB caps your best-possible LCP. |
| FCP First Contentful Paint |
first any content painted (text, image) | ≤ 1.8s | First sign of life; precursor to LCP. Lab and field. |
| TBT Total Blocking Time |
sum of long-task (>50ms) blocking between FCP and interactive | < 200ms (lab) | The lab proxy for INP. This is what you optimize in Lighthouse when field INP is poor. |
| TTI Time to Interactive |
main thread quiet ~5s after FCP | — | Deprecated — removed from Lighthouse 10 for being too variable. Say TBT instead. |
“TTFB → FCP → LCP is the loading timeline; TBT is the lab proxy for INP. TTI is legacy — Lighthouse dropped it for being too noisy, so I quote TBT now.”
Sources: web.dev — TTFB · web.dev — FCP · web.dev — TBT
| Common cause | Fix |
|---|---|
| Slow server / TTFB | CDN, edge cache, SSR streaming, faster backend |
| Render-blocking CSS/JS (Lesson 02!) | inline critical CSS, defer JS |
| LCP image discovered late / low priority | preload + fetchpriority=high (Lesson 01!), AVIF/WebP, srcset |
| Lazy-loading the hero | never lazy-load above-the-fold; lazy-load below it |
INP is the current interactivity metric — it replaced FID in March 2024. FID only measured the first interaction's input delay; INP measures the full latency of (nearly) every interaction all visit, so it's far harder to game. [web.dev]
An interaction's latency has three phases — naming them is the Lead-level answer:
| Cause | Fix |
|---|---|
| Long tasks hog the main thread (input delay) | break up long tasks, yield / scheduler.postTask, less JS |
| Heavy event handlers (processing) | do the minimum sync work; defer non-urgent work, debounce |
| Big synchronous re-render (presentation) | virtualize lists, content-visibility, move work off the click path |
| Hydration / third-party JS blocking | code-split, partial/progressive hydration, audit tags |
React 18's concurrent features map straight onto the three phases — the click path stays cheap, the expensive work goes non-urgent.
| Input delay | lazy+Suspense code-split, trim hydration (RSC / partial), less JS on the click
path |
| Processing | startTransition (mark the heavy state update non-urgent so the keystroke paints first),
useDeferredValue (defer expensive derived UI), useMemo / Web Worker for heavy
compute |
| Presentation | virtualize lists (react-window), React.memo + stable keys,
content-visibility:auto |
// search filter that janks on every keystroke
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query); // input stays snappy
const results = useMemo(() => filterBig(deferredQuery), [deferredQuery]);
// onChange just does setQuery(...) — cheap + urgent
“INP in React = keep the click path cheap. startTransition/useDeferredValue
make heavy updates non-urgent, virtualize big lists, Web Worker the heavy compute. The urgent paint never waits
on the expensive render.”
| Common cause | Fix |
|---|---|
| Images/videos with no dimensions | always set width/height or aspect-ratio |
| Ads / embeds / late content injected | reserve space with a min-height placeholder |
| Web fonts swapping (FOUT) | font-display + size-adjust to match metrics |
| Inserting content above existing content | don't; or use transform (composite-only, no shift) |
0.75 × 0.25 = 0.1875.Sources: web.dev — CLS · web.dev — INP · Google — Core Web Vitals · corewebvitals.io
The toolkit is explicit that they grade systemic thinking, not “I lazy-loaded an image.” Tie the three vitals to the platform and to a guardrail:
fetchpriority=high
the first photo, AVIF via CDN resizing.aspect-ratio on every photo, reserved space for
prices/badges that load late.size-limit,
webpack performance.maxAssetSize).// budget.json — sizes (KB) / counts / timings (ms), by path
[{ "path": "/*",
"resourceSizes": [{ "resourceType": "script", "budget": 300 },
{ "resourceType": "total", "budget": 600 }],
"timings": [{ "metric": "interactive", "budget": 3000 }] }]
// OR lighthouserc.js assertions — gate audit scores/metrics directly
assert: { assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }] }}
Runs via @lhci/cli in CI and fails the build when exceeded. Gotchas: it's
budget.json or assertions, not both; budget.json can only assert sizes/counts,
not metric scores like LCP.
“Bundle budget gates the artifact; Lighthouse budget gates the rendered page — real LCP/CLS and total weight including images & third-party. I run both in CI so neither blind spot ships.”
Concept: LCP/INP/CLS = load/react/stability, graded on p75 field data. Trade-off: chasing a lab Lighthouse score can diverge from real users — over-optimizing the median while the slow quarter (real devices, INP) stays red. Anchor: “Our Lighthouse was green but field INP was poor; the culprit was a third-party tag's long tasks — we deferred it and split our bundle.” Impact: CWV is a Google ranking signal and correlates with conversion on a booking funnel. Invite: “I'd weight INP higher on an interactive app, LCP higher on a content/SEO page.”
Pick an answer; instant feedback. Push-back style, like the round.
1. Match the “good” thresholds.
2. Your Lighthouse score is 98 (green) but the field report flags INP as poor. Most likely explanation?
They're testing lab-vs-field understanding.
3. INP is poor. The flame chart shows interactions wait ~250ms before the handler even starts. Which phase, and a fix?
4. Hotel cards jump down as each photo loads. Which vital, and the fix?
5. Why did Google replace FID with INP in 2024?
6. Best Lead answer to “how do you keep Core Web Vitals good across many teams?”
7. Field INP is poor, but you can only test in Lighthouse right now. Which lab metric do you
optimize, and what about TTI?
“You're current” probe — they want to hear TTI is legacy.
8. LCP is 5s. The waterfall shows TTFB alone is 2.6s. First thing you address?
9. An element covering 50% of the viewport shifts down by 20% of viewport height. The layout shift score for that frame?
10. A React search box janks on every keystroke because it filters a 5,000-item list synchronously. Best INP fix?
0 / 10 answered