The time it takes for the largest visible element to render in the viewport.
┌─────────────────────────────────────────────────────────────┐
│ Timeline │
│ │
│ 0ms ─────────────────────────────────────────────▶ 2500ms │
│ │ │ │ │
│ │ │ └── LCP: Hero image │
│ │ │ fully painted │
│ │ │ │
│ │ └── FCP: First text painted │
│ │ │
│ └── TTFB: First byte received │
│ │
│ What counts as LCP element: │
│ ├── <img> elements │
│ ├── <image> inside <svg> │
│ ├── <video> poster image │
│ ├── Background image via CSS url() │
│ └── Block-level text elements (<h1>, <p>, etc.) │
└─────────────────────────────────────────────────────────────┘
Measuring LCP
// Using web-vitals libraryimport{onLCP}from'web-vitals';onLCP((metric)=>{console.log('LCP:',metric.value);console.log('LCP Element:',metric.entries[0]?.element);console.log('Rating:',metric.rating);// 'good', 'needs-improvement', 'poor'// Send to analyticssendToAnalytics({name:'LCP',value:metric.value,id:metric.id,rating:metric.rating});});
// Using PerformanceObserver directlyconstobserver=newPerformanceObserver((list)=>{constentries=list.getEntries();constlastEntry=entries[entries.length-1];console.log('LCP:',lastEntry.startTime);console.log('Element:',lastEntry.element);});observer.observe({type:'largest-contentful-paint',buffered:true});
Optimizing LCP
Cause
Solution
Slow server response
CDN, edge caching, optimize backend
Render-blocking resources
Inline critical CSS, defer JS
Slow resource load
Preload LCP image, use CDN
Client-side rendering
SSR/SSG for above-fold content
<!-- Preload the LCP image --><linkrel="preload"as="image"href="/hero.jpg"fetchpriority="high"><!-- For responsive images --><linkrel="preload"as="image"href="/hero.jpg"imagesrcset="hero-400.jpg 400w, hero-800.jpg 800w"imagesizes="100vw">
INP measures the latency of all user interactions throughout the page lifecycle and reports the worst one (at the 98th percentile).
User clicks button
│
▼
┌──────────────────┐
│ Input Delay │ ← Time waiting in queue (main thread busy)
│ (event queued) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Processing Time │ ← Event handler execution time
│ (handler runs) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Presentation │ ← Time for browser to paint the result
│ Delay │
└────────┬─────────┘
│
▼
Next Paint
INP = Input Delay + Processing Time + Presentation Delay
Why INP Replaced FID
Metric
What It Measures
Problem
FID
Only FIRST interaction delay
Easy to game (fast initial load, slow later)
INP
ALL interactions, reports worst
Measures real user experience
Measuring INP
import{onINP}from'web-vitals';onINP((metric)=>{console.log('INP:',metric.value);console.log('Rating:',metric.rating);// The interaction that caused the worst INPconstentry=metric.entries[0];console.log('Interaction target:',entry.target);console.log('Interaction type:',entry.name);// 'click', 'keydown', etc.});
// ❌ BAD - Long task blocks main threadbutton.addEventListener('click',()=>{// 200ms of synchronous workprocessLargeDataset(data);updateUI();});// ✅ GOOD - Yield to main threadbutton.addEventListener('click',async ()=>{// Show immediate feedbackbutton.classList.add('loading');// Yield control back to browserawaitscheduler.yield?.()||newPromise(r=>setTimeout(r,0));// Do heavy workprocessLargeDataset(data);updateUI();});
// ✅ BETTER - Use Web Worker for heavy computationconstworker=newWorker('processor.js');button.addEventListener('click',()=>{button.classList.add('loading');worker.postMessage(data);});worker.onmessage=(e)=>{updateUI(e.data);button.classList.remove('loading');};
// ✅ Break up work with requestIdleCallbackfunctionprocessInChunks(items,callback){constqueue=[...items];functionprocessNext(deadline){while (queue.length>0&&deadline.timeRemaining()>0){constitem=queue.shift();callback(item);}if (queue.length>0){requestIdleCallback(processNext);}}requestIdleCallback(processNext);}
Cause
Solution
Long event handlers
Break into smaller tasks, yield
Heavy computation
Move to Web Worker
Large DOM updates
Virtual DOM, batch updates
Third-party scripts
Defer, facade pattern
4. CLS (Cumulative Layout Shift)
What It Measures
CLS quantifies how much visible elements unexpectedly shift during page load.
┌─────────────────────────────────────────────────────────────┐
│ Before Ad Loads After Ad Loads │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ Header │ │ Header │ │
│ ├────────────────┤ ├────────────────┤ │
│ │ Article │ │ AD │ ← Inserted │
│ │ Content │ ├────────────────┤ │
│ │ │ │ Article │ ← Shifted! │
│ │ [Button] │ │ Content │ │
│ └────────────────┘ │ [Button] │ ← Misclick!│
│ └────────────────┘ │
│ │
│ CLS Score = Impact Fraction × Distance Fraction │
│ │
│ Impact: % of viewport affected │
│ Distance: How far elements moved (as % of viewport) │
└─────────────────────────────────────────────────────────────┘
The CLS Formula
Layout Shift Score = Impact Fraction × Distance Fraction
Impact Fraction = (Area of shifted elements) / (Viewport area)
Distance Fraction = (Max distance moved) / (Viewport height or width)
Example:
- Element covers 50% of viewport (impact = 0.5)
- Element moves 25% of viewport height (distance = 0.25)
- Score = 0.5 × 0.25 = 0.125
// Using PerformanceObserverletclsValue=0;letclsEntries=[];constobserver=newPerformanceObserver((list)=>{for (constentryoflist.getEntries()){// Only count unexpected shifts (not from user input)if (!entry.hadRecentInput){clsValue+=entry.value;clsEntries.push(entry);}}});observer.observe({type:'layout-shift',buffered:true});
Optimizing CLS
<!-- ✅ Reserve space for images with aspect-ratio --><imgsrc="photo.jpg"width="800"height="600"style="aspect-ratio: 4/3; width: 100%; height: auto;"alt="Photo"><!-- ✅ Reserve space for ads --><divclass="ad-container"style="min-height: 250px;"><!-- Ad loads here --></div>
/* ✅ Prevent font swap layout shift */@font-face{font-family:'CustomFont';src:url('font.woff2')format('woff2');font-display:optional;/* or 'swap' with size-adjust */size-adjust:100.5%;/* Match fallback metrics */}
/* ✅ Use transform for animations (doesn't cause layout shift) */.animate{transform:translateY(-10px);/* Good */}.animate-bad{margin-top:-10px;/* Bad - causes layout shift */}
// Detect tasks blocking main thread > 50msconstobserver=newPerformanceObserver((list)=>{for (constentryoflist.getEntries()){console.warn(`Long task: ${entry.duration}ms`);// Get attribution if availableif (entry.attribution){console.log('Script:',entry.attribution[0]?.name);}}});observer.observe({type:'longtask',buffered:true});
6. Complete Measurement Setup
import{onLCP,onINP,onCLS,onFCP,onTTFB}from'web-vitals';functionsendToAnalytics(metric){constbody=JSON.stringify({name:metric.name,value:metric.value,rating:metric.rating,delta:metric.delta,id:metric.id,navigationType:metric.navigationType,// Include page contexturl:window.location.href,userAgent:navigator.userAgent,connection:navigator.connection?.effectiveType,deviceMemory:navigator.deviceMemory});// Use sendBeacon for reliability (survives page unload)if (navigator.sendBeacon){navigator.sendBeacon('/analytics/vitals',body);}else{fetch('/analytics/vitals',{body,method:'POST',keepalive:true});}}// Register all Core Web VitalsonLCP(sendToAnalytics);onINP(sendToAnalytics);onCLS(sendToAnalytics);// Additional helpful metricsonFCP(sendToAnalytics);onTTFB(sendToAnalytics);// Report only once per pageconstreported=newSet();functionsendOnce(metric){if (!reported.has(metric.name)){reported.add(metric.name);sendToAnalytics(metric);}}
┌─────────────────────────────────────────────────────────────┐
│ WHY THEY DIFFER │
│ │
│ Lab Data: │
│ - Simulated device/network │
│ - No real user interaction │
│ - Consistent, reproducible │
│ │
│ Field Data: │
│ - Real devices (slow phones!) │
│ - Real networks (3G in India!) │
│ - Real user behavior │
│ │
│ Field data is what Google uses for rankings! │
└─────────────────────────────────────────────────────────────┘
Set image dimensions, reserve ad space, use transform
10. Interview Tip
"I measure Core Web Vitals using the web-vitals library and send data to our analytics backend using sendBeacon for reliability. For LCP, I preload the hero image and inline critical CSS. For INP, I profile with DevTools to find long tasks and break them up using yield points or move heavy computation to Web Workers. For CLS, I ensure all images have explicit dimensions and reserve space for dynamic content like ads. I distinguish between lab and field data—Lighthouse is for debugging, but CrUX/RUM reflects real user experience and is what Google uses for rankings. We track P75 values and set alerts when they degrade."
Top comments (0)
Subscribe
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
Top comments (0)