Back to Blog
PerformanceCore Web VitalsWeb DevelopmentMetrics

Web Performance Metrics That Matter

Understand Core Web Vitals and beyond. From LCP to FID to CLS, measure what affects real user experience.

B
Bootspring Team
Engineering
April 5, 2024
5 min read

Performance metrics help you understand user experience. Focus on the metrics that correlate with real user satisfaction—Core Web Vitals and beyond.

Core Web Vitals#

LCP (Largest Contentful Paint) - Measures: Loading performance - Target: < 2.5 seconds - What: Time until largest content element renders FID (First Input Delay) - Measures: Interactivity - Target: < 100 milliseconds - What: Time from first interaction to response CLS (Cumulative Layout Shift) - Measures: Visual stability - Target: < 0.1 - What: Unexpected layout movements

Measuring Core Web Vitals#

1// Using web-vitals library 2import { getLCP, getFID, getCLS, onLCP, onFID, onCLS } from 'web-vitals'; 3 4function sendToAnalytics(metric: Metric) { 5 const body = JSON.stringify({ 6 name: metric.name, 7 value: metric.value, 8 rating: metric.rating, 9 delta: metric.delta, 10 id: metric.id, 11 }); 12 13 // Use sendBeacon for reliability 14 if (navigator.sendBeacon) { 15 navigator.sendBeacon('/analytics', body); 16 } else { 17 fetch('/analytics', { body, method: 'POST', keepalive: true }); 18 } 19} 20 21onLCP(sendToAnalytics); 22onFID(sendToAnalytics); 23onCLS(sendToAnalytics);

LCP Optimization#

1<!-- Preload LCP image --> 2<link rel="preload" as="image" href="/hero.webp"> 3 4<!-- Preconnect to critical origins --> 5<link rel="preconnect" href="https://fonts.googleapis.com"> 6<link rel="preconnect" href="https://cdn.example.com" crossorigin>
1// Identify LCP element 2new PerformanceObserver((entryList) => { 3 const entries = entryList.getEntries(); 4 const lastEntry = entries[entries.length - 1]; 5 console.log('LCP element:', lastEntry.element); 6 console.log('LCP time:', lastEntry.startTime); 7}).observe({ type: 'largest-contentful-paint', buffered: true });
LCP common causes: - Slow server response time - Render-blocking JavaScript/CSS - Slow resource load times - Client-side rendering LCP fixes: ✓ Use a CDN ✓ Cache static assets ✓ Preload critical resources ✓ Optimize images ✓ Use SSR or static generation

FID / INP Optimization#

1// INP (Interaction to Next Paint) is replacing FID 2 3// Break up long tasks 4function processLargeArray(items: Item[]) { 5 const CHUNK_SIZE = 100; 6 let index = 0; 7 8 function processChunk() { 9 const chunk = items.slice(index, index + CHUNK_SIZE); 10 chunk.forEach(processItem); 11 index += CHUNK_SIZE; 12 13 if (index < items.length) { 14 // Yield to main thread 15 requestIdleCallback(processChunk); 16 } 17 } 18 19 processChunk(); 20} 21 22// Use web workers for heavy computation 23const worker = new Worker('heavy-computation.js'); 24worker.postMessage(data); 25worker.onmessage = (e) => { 26 updateUI(e.data); 27};
FID/INP common causes: - Long JavaScript tasks - Heavy third-party scripts - Large bundle sizes - Main thread blocking FID/INP fixes: ✓ Code split and lazy load ✓ Use web workers ✓ Break up long tasks ✓ Defer non-critical JavaScript ✓ Remove unused code

CLS Optimization#

1/* Reserve space for images */ 2img { 3 aspect-ratio: 16 / 9; 4 width: 100%; 5 height: auto; 6} 7 8/* Reserve space for ads */ 9.ad-container { 10 min-height: 250px; 11} 12 13/* Avoid FOUT (Flash of Unstyled Text) */ 14@font-face { 15 font-family: 'CustomFont'; 16 font-display: swap; 17 src: url('/fonts/custom.woff2') format('woff2'); 18}
1<!-- Always include dimensions --> 2<img src="image.jpg" width="800" height="600" alt="Description"> 3 4<!-- Use aspect-ratio for responsive --> 5<img 6 src="image.jpg" 7 style="aspect-ratio: 4/3; width: 100%; height: auto;" 8 alt="Description" 9>
CLS common causes: - Images without dimensions - Ads, embeds without reserved space - Dynamically injected content - Web fonts causing FOUT - Animations that trigger layout CLS fixes: ✓ Always set image dimensions ✓ Reserve space for dynamic content ✓ Use CSS transforms instead of layout properties ✓ Preload fonts ✓ Use font-display: optional or swap

Other Important Metrics#

TTFB (Time to First Byte) - Server response time - Target: < 600ms FCP (First Contentful Paint) - First content rendered - Target: < 1.8s TTI (Time to Interactive) - Page fully interactive - Target: < 3.8s TBT (Total Blocking Time) - Sum of long task blocking time - Target: < 200ms Speed Index - How quickly content is visually displayed - Target: < 3.4s

Real User Monitoring (RUM)#

1// Collect real user metrics 2class RUMCollector { 3 private metrics: Record<string, number> = {}; 4 5 collect() { 6 this.collectNavigationTiming(); 7 this.collectResourceTiming(); 8 this.collectWebVitals(); 9 } 10 11 private collectNavigationTiming() { 12 const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; 13 14 this.metrics.dns = nav.domainLookupEnd - nav.domainLookupStart; 15 this.metrics.tcp = nav.connectEnd - nav.connectStart; 16 this.metrics.ttfb = nav.responseStart - nav.requestStart; 17 this.metrics.download = nav.responseEnd - nav.responseStart; 18 this.metrics.domParse = nav.domInteractive - nav.responseEnd; 19 this.metrics.domComplete = nav.domComplete - nav.domInteractive; 20 this.metrics.load = nav.loadEventEnd - nav.loadEventStart; 21 } 22 23 private collectResourceTiming() { 24 const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[]; 25 26 this.metrics.resourceCount = resources.length; 27 this.metrics.totalTransferSize = resources.reduce( 28 (sum, r) => sum + (r.transferSize || 0), 29 0 30 ); 31 } 32 33 private collectWebVitals() { 34 // Use web-vitals library 35 onLCP((metric) => { this.metrics.lcp = metric.value; }); 36 onFID((metric) => { this.metrics.fid = metric.value; }); 37 onCLS((metric) => { this.metrics.cls = metric.value; }); 38 } 39 40 send() { 41 navigator.sendBeacon('/rum', JSON.stringify({ 42 metrics: this.metrics, 43 url: location.href, 44 userAgent: navigator.userAgent, 45 connection: (navigator as any).connection?.effectiveType, 46 timestamp: Date.now(), 47 })); 48 } 49} 50 51// Collect on page load 52window.addEventListener('load', () => { 53 const rum = new RUMCollector(); 54 rum.collect(); 55 56 // Send before user leaves 57 document.addEventListener('visibilitychange', () => { 58 if (document.visibilityState === 'hidden') { 59 rum.send(); 60 } 61 }); 62});

Performance Budget#

1// lighthouse-budget.json 2[ 3 { 4 "resourceSizes": [ 5 { "resourceType": "script", "budget": 300 }, 6 { "resourceType": "stylesheet", "budget": 100 }, 7 { "resourceType": "image", "budget": 500 }, 8 { "resourceType": "font", "budget": 100 }, 9 { "resourceType": "total", "budget": 1000 } 10 ], 11 "resourceCounts": [ 12 { "resourceType": "script", "budget": 10 }, 13 { "resourceType": "stylesheet", "budget": 5 } 14 ], 15 "timings": [ 16 { "metric": "first-contentful-paint", "budget": 1800 }, 17 { "metric": "largest-contentful-paint", "budget": 2500 }, 18 { "metric": "cumulative-layout-shift", "budget": 0.1 }, 19 { "metric": "total-blocking-time", "budget": 200 } 20 ] 21 } 22]

Monitoring Dashboard#

Key metrics to track: - Core Web Vitals (p75) - TTFB (p95) - Error rates - Cache hit rates - JavaScript errors - API response times Segment by: - Device type (mobile/desktop) - Connection type (4G/3G/slow) - Geographic region - Browser - Page type

Conclusion#

Focus on Core Web Vitals—they correlate with real user experience and affect search rankings. Measure with real user monitoring, set performance budgets, and continuously optimize.

Performance isn't a feature you ship once—it's a practice you maintain.

Share this article

Help spread the word about Bootspring