Core Web Vitals & CRP
โฑ๏ธ ~3-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.๐ฉบ Vital signs for a webpage โ each metric answers a specific user experience question.
The five questions
Is the server fast?
User thinks: "How long before any HTML arrives?"
Is anything happening?
User thinks: "When does the blank screen go away?"
Is the main content here?
User thinks: "When can I read/see the page?"
Is the page stable?
User thinks: "Did content just jump when I tried to click?"
Is the page responsive?
User thinks: "Did my click/tap do anything?"
Critical Rendering Path (CRP)
Every web vital is shaped by the Critical Rendering Path โ the sequence of steps the browser must complete before pixels appear on screen.
Field data vs lab data
Lab data (Lighthouse)
- โข Runs in a controlled environment
- โข Throttled CPU + network
- โข Reproducible โ good for debugging
- โข Doesn't reflect real users
- โข CLS/INP often inaccurate in lab
Field data (CrUX)
- โข Real Chrome users (28-day window)
- โข Accounts for all devices/networks
- โข What Google uses for ranking
- โข 28-day lag โ slow feedback loop
- โข web-vitals.js measures in-page
Interactive Sandbox
โ Move something, see it react instantly.Core Web Vital
LCP
Largest Contentful Paint
Time from navigation to largest image or text block rendered in the viewport
45
Poor
demo score
Page load timeline โ when LCP fires
LCP: Largest element visible (~2200ms into load)
Top causes of poor LCP
โHero image not preloaded (discovered late in CSS)
โNo CDN for images โ slow origin server
โClient-side rendered content (empty HTML shell)
Top fixes
โ<link rel='preload' as='image'> for hero images
โUse next/image with priority prop for above-fold images
โServer-render the LCP element (not client-side)
Challenge
Explore all 5 Core Web Vitals. For each one, answer: what user experience does it measure, and what's the single most impactful fix?
Why Should I Care?
โ The exact interview question + the bug it kills.Interview questions
Q: How does LCP differ from FCP?
FCP fires when ANY content is painted โ including spinners, skeletons, or placeholder text. LCP fires when the largest text block or image in the viewport is painted. A page with a fast-loading spinner but slow hero image can have FCP at 0.8s (good) and LCP at 4.5s (poor). LCP is a better proxy for when the page is "actually useful" to the user.
Q: What is INP and why did it replace FID?
First Input Delay (FID) measured only the FIRST interaction on the page, and only the delay before the browser could start handling it (not the full processing + paint time). INP measures ALL interactions throughout the page lifetime (clicks, taps, keyboard events) and reports the worst one. An e-commerce product page might pass FID but fail INP because the "Add to Cart" click on the third interaction is sluggish.
Bug: Images without width/height cause CLS
| 1 | <!-- โ No dimensions โ browser allocates 0px until image loads --> |
| 2 | <img src="/hero.jpg" alt="Hero" /> |
| 3 | <!-- When image loads, layout shifts โ CLS penalty --> |
| 4 | ย |
| 5 | <!-- โ Always set width + height --> |
| 6 | <img src="/hero.jpg" alt="Hero" width="1200" height="600" /> |
| 7 | <!-- Browser reserves space โ no layout shift --> |
| 8 | ย |
| 9 | <!-- โ Next.js: use next/image (sets dimensions + lazy-loads) --> |
| 10 | <Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority /> |
The Deep Dive
โ Spec refs, engine internals, the minutiae.Measuring with web-vitals.js
| 1 | import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals'; |
| 2 | ย |
| 3 | function sendToAnalytics(metric) { |
| 4 | const { name, value, rating, id } = metric; |
| 5 | // rating: 'good' | 'needs-improvement' | 'poor' |
| 6 | navigator.sendBeacon('/analytics', JSON.stringify({ |
| 7 | metric: name, value, rating, url: location.href, |
| 8 | })); |
| 9 | } |
| 10 | ย |
| 11 | onLCP(sendToAnalytics); |
| 12 | onINP(sendToAnalytics); |
| 13 | onCLS(sendToAnalytics); |
| 14 | onFCP(sendToAnalytics); |
| 15 | onTTFB(sendToAnalytics); |
PerformanceObserver โ measuring Long Tasks for INP
| 1 | // Long Tasks API โ tasks > 50ms block INP |
| 2 | const observer = new PerformanceObserver((list) => { |
| 3 | list.getEntries().forEach((entry) => { |
| 4 | console.log('Long Task:', entry.duration.toFixed(0) + 'ms'); |
| 5 | // attribution: which script/function caused it |
| 6 | entry.attribution.forEach((attr) => { |
| 7 | console.log(' โ', attr.name, attr.containerSrc); |
| 8 | }); |
| 9 | }); |
| 10 | }); |
| 11 | observer.observe({ type: 'longtask', buffered: true }); |
| 12 | ย |
| 13 | // Also: Event Timing API (measures interaction latency directly) |
| 14 | const evObserver = new PerformanceObserver((list) => { |
| 15 | list.getEntries().forEach((entry) => { |
| 16 | if (entry.duration > 200) { // INP threshold |
| 17 | console.log('Slow interaction:', entry.name, entry.duration); |
| 18 | } |
| 19 | }); |
| 20 | }); |
| 21 | evObserver.observe({ type: 'event', buffered: true, durationThreshold: 16 }); |
Core Web Vitals as a Google ranking signal
Since June 2021, Google incorporates CWV into the "page experience" ranking signal. All three Core Web Vitals (LCP, INP since 2024, CLS) must be in the "Good" range for 75% of page loads (75th percentile of field data) to get the maximum ranking benefit. Lab scores (Lighthouse) do NOT affect rankings โ only CrUX field data matters.
Interview Questions
โ Real questions from real interviews โ with answers.LCP measures when the largest image or text block in the viewport is painted โ poor scores are usually caused by unpreloaded hero images or client-side rendered content.
CLS measures cumulative visual instability. Fix images by setting explicit dimensions; fix fonts with font-display: swap or preloading.
FID measured only the first interaction and only the input delay, not the full processing + paint time. INP measures all interactions and picks the worst.
The CRP is the sequence of steps โ DNS, TCP, HTML parse, CSSOM build, JS execution โ that must complete before the first pixel is painted.
Lab data is synthetic (throttled, reproducible); field data is real Chrome users over 28 days. Only field data from CrUX affects Google rankings.
Identify long tasks blocking the main thread with PerformanceObserver / Chrome DevTools, then break them with scheduler.yield(), memoization, or Web Workers.
Memory Game
โ Quick quiz โ lock the concept in long-term memory.What is a Long Task as defined by the browser?