The Metrics — Lab vs Field
FCP, LCP, CLS, INP, TTFB, TBT — thresholds, percentiles, PerformanceObserver, and the web-vitals library.
Metrics are only useful when you know what they measure, where (lab vs field), and how to act on them.
Lab vs field vs synthetic
| Type | Source | Strength | Weakness |
|---|---|---|---|
| Lab | Lighthouse, WebPageTest, local trace | Reproducible, debuggable | May not match real devices/networks |
| Field (RUM) | web-vitals, your analytics, CrUX | Real users, real conditions | Noisy; needs volume |
| Synthetic | Scheduled crawlers | Trend monitoring | Not your logged-in flows |
Core Web Vitals (Google’s UX signals) for ranking/guidance: LCP, INP, CLS — evaluated at 75th percentile (p75) of page loads.
Metric reference
TTFB — Time to First Byte
Server + network latency before HTML bytes arrive. Sub-metrics: DNS, connection, waiting (TTFB proper), download.
Good ≤ 800 ms · Poor > 1800 ms
FCP — First Contentful Paint
First text or image painted. Proxy for “something appeared.”
Good ≤ 1800 ms · Poor > 3000 ms
LCP — Largest Contentful Paint
Largest visible image or text block in viewport. Not always the hero — can be a paragraph.
Good ≤ 2500 ms · Poor > 4000 ms
LCP candidates: <img>, <image> in SVG, poster images, block-level elements with background images, text nodes in block containers.
INP — Interaction to Next Paint
Replaced FID in 2024. p98 of interaction latencies across page lifetime (worst-ish interaction, not average).
Good ≤ 200 ms · Poor > 500 ms
Breakdown per interaction:
CLS — Cumulative Layout Shift
Sum of impact fraction × distance fraction for unexpected shifts (not user-initiated within 500 ms).
Good ≤ 0.1 · Poor > 0.25
TBT — Total Blocking Time (lab)
Sum of (task duration − 50 ms) for tasks between FCP and TTI. Correlates with INP but measured in lab traces.
Good ≤ 200 ms · Poor > 600 ms
Measuring in production
Use the web-vitals library — it handles visibility, bfcache, and metric-specific quirks:
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';
function sendToAnalytics({ name, value, id, rating, attribution }) {
// Your endpoint — include attribution in debug builds
navigator.sendBeacon('/analytics', JSON.stringify({ name, value, id, rating, attribution }));
}
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
Attribution build (web-vitals/attribution) adds element selectors, URLs, and phase breakdowns — essential for debugging LCP/INP in production.
PerformanceObserver (lower level)
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, entry.startTime, entry.duration);
}
});
po.observe({ type: 'longtask', buffered: true });
po.observe({ type: 'event', buffered: true, durationThreshold: 16 });
| Entry type | Use for |
|---|---|
navigation | TTFB, redirect timing |
resource | Slow assets |
longtask | Main-thread blocking |
event | INP debugging (with durationThreshold) |
layout-shift | CLS sources |
Distributions beat averages
Report histograms or percentiles (p50, p75, p95), segmented by:
- Device class (mobile vs desktop)
- Connection (
navigator.connection?.effectiveType) - Route / template
- Release version
Averages hide tail latency where INP and support tickets live.
Checklist before optimizing
- Is the regression lab-only or confirmed in RUM?
- Which phase (load vs interaction vs stability)?
- Can you attribute to an element, script URL, or long task?
- What’s the business route priority?
Next: Module 02 — loading metrics (TTFB, FCP, LCP). Module 03 — INP and the main thread.