Core Web Vitals — LCP, INP, CLS
TL;DR
Three Google-defined metrics that gate “good” page performance: LCP (Largest Contentful Paint — when the main content visibly appears), INP (Interaction to Next Paint — how snappy interactions feel), and CLS (Cumulative Layout Shift — visual stability). They’re measured at the 75th percentile of real users, not in lab tools. Lab tools (Lighthouse) measure proxies; field tools (CrUX, web-vitals.js) measure reality. INP replaced FID in March 2024.
Interview Q&A
Q: What are the three Core Web Vitals and their thresholds?
A:
| Metric | What it measures | Good (p75) | Needs improvement | Poor |
|---|---|---|---|---|
| LCP | time to render the largest in-viewport element (image/text/video block) | ≤ 2.5s | ≤ 4.0s | > 4.0s |
| INP | latency of the worst user interaction (click/tap/keypress → next paint) | ≤ 200ms | ≤ 500ms | > 500ms |
| CLS | sum of unexpected layout shifts (visual jank) | ≤ 0.1 | ≤ 0.25 | > 0.25 |
All three at “Good” = page is “Good.” Any one in “Poor” = page is “Poor.”
Q: What is LCP and what kills it?
A: The render time of the largest text block or image visible in the initial viewport. Common causes of poor LCP:
- Slow server response (TTFB). Optimize backend, use a CDN, push-cache.
- Render-blocking CSS/JS. Inline critical CSS; defer non-critical.
- Large images loaded slowly. Compress, modern formats (AVIF/WebP),
<picture>+srcset,fetchpriority="high"on the LCP image. - Client-side rendering blocking text paint. SSR or pre-render the above-the-fold content.
The fix flow:
- Identify the LCP element (Chrome DevTools → Performance → LCP marker).
- Trace why it loaded late — DNS? Connection? Server? Render-blocking resource? JS?
- Optimize the slowest segment.
fetchpriority="high" on the hero image is the cheapest LCP win — tells the browser to deprioritize other resources for this one.
Q: What is INP and what kills it?
A: INP measures the latency from a user input to the next paint, across the worst interaction on the page (not the average). Long INP means clicking/tapping feels laggy.
Causes:
- Long JavaScript tasks (>50ms) blocking the main thread.
- Heavy event handlers doing too much synchronous work.
- Layout thrashing — reading then writing layout properties in a loop.
- Synchronous third-party scripts (analytics, A/B testing libraries).
Fixes:
- Break up long tasks —
setTimeout,scheduler.yield(),requestIdleCallback. - Move work off the main thread with Web Workers.
useTransitionin React 18+ marks an update non-urgent so it doesn’t block typing.- Debounce expensive handlers.
Q: What is CLS and what kills it?
A: Sum of all unexpected layout shifts during the page’s lifetime (specifically: the largest shift in each session window). 0 is perfect; anything > 0.1 is noticeable.
Causes:
- Images without dimensions — when the image loads, it pushes content down.
- Ads / iframes without reserved space.
- Web fonts causing FOIT/FOUT — text wraps differently when the font loads.
- Dynamic content injected above the fold (cookie banner, sticky header appearing).
Fixes:
width/height/aspect-ratioon every image — browser reserves the box.min-heighton ad/iframe containers.font-display: swap+size-adjustto match fallback font metrics.- Skeleton loaders during data fetch instead of “empty → full” pop-in.
Q: How do you measure Core Web Vitals in production?
A: Two complementary sources:
-
Field data (real users):
- CrUX (Chrome User Experience Report) — aggregated real-user data from Chrome, queryable via PageSpeed Insights API or BigQuery.
web-vitalslibrary — ships in your page, reports each metric to your analytics:import { onLCP, onINP, onCLS } from "web-vitals/attribution"; onLCP((m) => sendToAnalytics({ name: "LCP", value: m.value, attribution: m.attribution })); onINP((m) => sendToAnalytics({ name: "INP", value: m.value })); onCLS((m) => sendToAnalytics({ name: "CLS", value: m.value }));- The
/attributionvariant includes why it was slow — element selector, load source, the slow interaction’s element.
-
Lab data (controlled environment):
- Lighthouse / PageSpeed Insights — simulated load, useful for regression testing.
- WebPageTest — more realistic conditions, multiple test locations.
- Chrome DevTools → Performance / Lighthouse panels — local debugging.
Field data is what Google rewards (search ranking). Lab data is what you debug.
Q: Why did INP replace FID?
A: First Input Delay (FID) measured only the first interaction’s input delay (the time from event to handler running). Two problems:
- It only measured the first — late-page interactions (scroll, click) were invisible.
- It only measured input delay (browser dispatching the event), not the full interaction latency (event → handler → re-render → paint).
INP (Interaction to Next Paint) measures the worst interaction’s full lifecycle. It’s a fairer reflection of perceived responsiveness. Replaced FID as a Core Web Vital on March 12, 2024.
Q: What’s the relationship between TTFB, FCP, and LCP?
A: Sequential in the load timeline:
| Metric | Definition |
|---|---|
| TTFB (Time To First Byte) | request fires → first byte of response |
| FCP (First Contentful Paint) | first DOM text/image painted |
| LCP (Largest Contentful Paint) | largest text/image fully painted |
Improving TTFB benefits everything downstream. FCP is a useful signal but not a Core Web Vital itself.
Q: How do you debug a poor LCP?
A:
- Open DevTools → Performance, record a page load.
- Look for the LCP marker — Chrome highlights the LCP element in the screenshot.
- Check the network timeline: when did the LCP resource start? Why so late?
- If it’s an image discovered late by the preload scanner: add
<link rel="preload" as="image">orfetchpriority="high". - If the response started late (high TTFB): backend / CDN issue.
- If it’s a text element waiting for the web font: optimize font loading (see 07_image_and_font_optimization.md).
- If it’s an image discovered late by the preload scanner: add
- Look for render-blocking resources — CSS/JS in
<head>that block paint. Inline critical CSS, defer non-critical, async/defer scripts. - Confirm with field data — local fixes don’t always reflect what real users see.
Q: A site has a great LCP but a terrible INP. Where do you look?
A: Long tasks. Open Performance → record a click → find the long task blocking the event handler or the next paint.
- Third-party scripts (analytics, ads, chat widgets) are common culprits — they hijack the main thread.
- Large hydration in SSR apps — the page renders but isn’t interactive because hydration is busy.
- React’s render after
setStateif the work is heavy —useTransition/useDeferredValueto defer non-urgent work.
The attribution variant of web-vitals tells you which element was clicked and what handler ran — start there.
Q: What’s a realistic performance budget you’d set?
A: (See README table.) Per-route, per-metric:
- LCP < 2.5s p75 across all routes (Good).
- INP < 200ms p75.
- CLS < 0.1 p75.
- Per-route JS bundle ceiling (e.g., < 200 KB transferred).
- CI gate: fail the build if Lighthouse CI or bundle-size check exceeds the budget.
A budget without enforcement drifts the moment people are busy. The senior answer is always: budget + measurement + CI gate.
Gotchas / edge cases
- CLS isn’t “cumulative across the whole page lifetime” anymore — since 2021, it’s “the largest shift cluster in any 5s session window.” Long-lived SPAs aren’t penalized for occasional cumulative drift.
- INP requires Chrome 96+ field data — the metric exists in older browsers but isn’t reported the same way.
- Lab LCP ≠ field LCP — lab simulates one load; field reflects warmed caches, varied networks, etc.
<img loading="lazy">on the LCP image hurts LCP — lazy-loading delays the very thing you’re trying to render fast. Eager +fetchpriority="high"for the hero.requestAnimationFramecallbacks count toward INP if they’re after an interaction. Long rAF work after a click adds latency.- CLS during navigation (Next.js route change, etc.) doesn’t count — it’s only “unexpected” shifts on a stable page.
What a senior is expected to say
- “Three vitals: LCP (visible content), INP (responsiveness), CLS (stability). Each measured at p75 of real users — field data, not lab. INP replaced FID in 2024.”
- “Budget per route, per metric, gated in CI. A budget without enforcement is a wish.”
- “LCP: identify the element, trace why it loaded late (TTFB, render-block, image discovery).
fetchpriority='high'on the hero is the cheapest win.” - “INP: long tasks blocking the main thread. Break up work with
scheduler.yield, move heavy work to Workers, mark non-urgent updates withuseTransition.” - “CLS: reserve space — width/height/aspect-ratio on images, min-height on dynamic containers,
font-display: swap+ matching metrics.” - “I use
web-vitalswith/attributionto capture not just the metric but what caused it — element selector, slow handler, etc.”
Cross-references
- Rendering pipeline that determines LCP/CLS: 02_critical_rendering_path.md
- Image and font perf (most LCP/CLS issues live here): 07_image_and_font_optimization.md
- Resource hints to fix late-discovered LCP resources: 08_resource_hints.md
- React-specific INP via
useTransition: ../05_react/
Further reading
- web.dev — Core Web Vitals: https://web.dev/articles/vitals
- web.dev — INP: https://web.dev/articles/inp
web-vitalslibrary: https://github.com/GoogleChrome/web-vitals- CrUX (real-user data): https://developer.chrome.com/docs/crux/overview