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.