Field Guide · Vol. 3
Measure → Diagnose → Fix

Performance Metrics, in focus.

Six numbers decide how fast a page feels — not how fast it technically loads. This is what each one measures, the threshold that counts as "good", and the specific lever that moves it.

PT.1 · The Metrics

Six numbers that define how fast it feels

Each metric fires at a different point in the page lifecycle — from the server's first byte to the user's first tap. Read them left-to-right as a timeline of perceived speed.

When each metric fires

TTFB FCP LCP TBT window INP · CLS Nav start First byte First paint Interactive Full lifetime →

Fig. 1 — TTFB and FCP gate the start; LCP marks the main content; INP and CLS are measured across the entire page lifetime, not just load.

The thresholds, at a glance

Metric What it measures Good Needs work Poor
TTFB
Time to First Byte
How quickly the server starts replying ≤ 800ms ≤ 1.8s > 1.8s
FCP
First Contentful Paint
When the user sees anything at all ≤ 1.8s ≤ 3s > 3s
LCP CWV
Largest Contentful Paint
When the main content lands ≤ 2.5s ≤ 4s > 4s
CLS CWV
Cumulative Layout Shift
How much the layout jumps while loading ≤ 0.1 ≤ 0.25 > 0.25
INP CWV
Interaction to Next Paint
Whether every tap and click feels instant ≤ 200ms ≤ 500ms > 500ms
TBT
Total Blocking Time
How long the main thread is hogged during load ≤ 200ms ≤ 600ms > 600ms

Fig. 2 — The three marked CWV are Google's Core Web Vitals and feed Search ranking. Thresholds are p75 field data.

What each one is really telling you

TTFB — the upstream ceiling
Gap between the request leaving the browser and the first response byte arriving. Nothing renders before this. High TTFB is almost always server-side: DB query bottlenecks, CDN misses, redirect chains, missing edge caching.
FCP — has the page started?
Fires on the first pixel of any text, image, SVG, or canvas — even a spinner counts. Killed by render-blocking <link> / <script> in <head>, unoptimised critical CSS, and font-display: block.
LCP — the main content moment
Timestamp when the largest visible element (hero image, heading, video poster) fully renders. Chrome tracks candidates and locks in on first interaction. Bottlenecked by unoptimised hero images, missing fetchpriority="high", or SSR hydration delays.
CLS — does it jump?
Unitless score = impact fraction × distance fraction per shift, summed over the worst 5-second session window. Shifts within 500ms of input are excluded. Causes: images without dimensions, late fonts, banners injected above content.
INP — does interaction feel instant?
Replaced FID in March 2024. The 98th-percentile interaction latency across the whole page life. Each interaction = input delay + processing time + presentation delay — all three must fit in 200ms. Heavy re-renders and long tasks are the offenders.
TBT — the lab proxy for INP
Sum of the blocking part of every long task (>50ms) between FCP and TTI. A lab-only metric (Lighthouse): the best stand-in for INP where real interaction data doesn't exist. Dominated by JS parse + execution cost.
PT.2 · How to Improve

Fix the metric, not just the score

Each metric has a small set of high-leverage fixes. Work backward from the symptom to the cause, in roughly this order of impact.

Fixing LCP — get the largest element painted in 2.5s

Prioritise the LCP resource fetch
Add fetchpriority="high" to the hero <img> and loading="eager" (never lazy above the fold). For CSS background images, add a <link rel="preload"> in <head>. Impact: very high.
Serve next-gen formats from a CDN
AVIF → WebP → JPEG fallback via <picture> + responsive srcset, from the nearest edge. A 2MB hero JPEG that becomes a 180KB AVIF shaves ~800ms on median mobile. Impact: high.
Flatten the resource chain
Each extra redirect costs a full RTT; third-party scripts that inject the LCP image add 2–3 hops. Use DevTools → Performance → LCP waterfall to find and shorten the chain. Impact: high.
SSR or static-generate text LCP
If the LCP element is a heading injected by JS (common in SPAs), it can't paint until hydration finishes. Put it in the HTML payload so it's paintable right after TTFB + parse. Impact: high for SPAs.

Fixing INP — keep all three phases under 200ms

Yield to the main thread inside handlers
await scheduler.yield() (Chrome 115+) or setTimeout(0) breaks long handlers into chunks, letting the browser paint between tasks. Impact: very high.
Defer non-urgent React updates
Wrap non-critical state updates (list filtering, search results) in startTransition() so the click feedback paints first. Impact: high in React apps.
Offload heavy compute to Web Workers
Anything CPU-heavy that doesn't touch the DOM (parsing, crypto, search indexing, wasm) runs off-thread; post results back via postMessage. Impact: high for compute-heavy pages.
Avoid forced synchronous layout
Reading offsetHeight / getBoundingClientRect right after a style write forces a sync recalc. Batch reads before writes, or schedule reads in requestAnimationFrame. Impact: medium–high.

Fixing CLS — reserve the space upfront

Set explicit width and height on every image
Without dimensions the browser reserves 0×0, then expands on load — shifting everything below. width/height attrs (or CSS aspect-ratio) let it allocate space ahead of time. Impact: very high — the most common cause.
Use font-display: optional
Uses the web font only if already cached, else the system font — eliminating FOUT shifts. For brand fonts, match metrics with size-adjust + ascent-override. Impact: high.
Reserve space for dynamic content
Cookie banners, ad slots, and embeds injected above content are the biggest villains. Set min-height on their containers, or use skeletons matching final dimensions. Impact: high.

Fixing TTFB — a server and network concern

Cache at the edge with a CDN
A page from an edge node 20ms away always beats a single-region origin. Use long Cache-Control with stale-while-revalidate to serve instantly while refreshing in the background. Impact: very high.
Profile DB queries on the critical path
For dynamic pages, TTFB is dominated by waiting on data. Use EXPLAIN plans, index WHERE/JOIN columns, and consider read replicas for distributed traffic. Impact: high for dynamic pages.
Use 103 Early Hints
103 Early Hints lets the server send preload hints for fonts and hero images before the 200 OK is ready, so the browser fetches during server think time — shaving 200–500ms off perceived LCP. Impact: medium–high.

Fixing TBT — ship less JS, or run it later

Aggressively code-split
Route- and component-level dynamic import() means only the current page's code loads upfront. An 800KB main bundle where 200KB suffices wastes 300–600ms of parse + eval. Impact: very high.
Audit and tame third-party scripts
Analytics, chat, A/B, and ad scripts routinely add 200–800ms of blocking. Use async/defer, load after interaction (facade pattern), or swap for lighter alternatives. Impact: high.
Tree-shake and drop legacy polyfills
Set a modern browserslist target (no IE11) and check the bundle for core-js polyfilling APIs you already support. @babel/preset-env with useBuiltIns: 'usage' + corejs: 3 keeps it surgical. Impact: medium.
PT.3 · Accelerated Mobile Pages

What is AMP, and does it still matter?

AMP is an open-source HTML framework Google launched in 2015. Instead of hoping developers write fast pages, it makes it structurally impossible to write slow ones — by banning custom JS, requiring async-only scripts, pre-sizing all media, and serving pages from the Google AMP Cache built into Search.

The constraints AMP enforces

No custom JavaScript
All JS comes from amp-* components run by the AMP runtime; custom <script> fails validation. This eliminates TBT entirely by design.
Mandatory layout attributes
Every media element must declare layout, width, and height before rendering — structurally preventing CLS.
Google AMP Cache
Tapping an AMP result serves the page from Google's CDN, preloaded in the background. TTFB is effectively zero because it's already in memory.
Inline critical CSS only
Max 75KB of inline CSS, no external stylesheets (except Google Fonts) — forcing critical-CSS-only and eliminating render-blocking stylesheets.
Validator-enforced
Pages must pass the AMP validator — a hard spec that acts like a performance linter in the deploy pipeline. You literally can't ship a slow AMP page.

Typical metric comparison (p75)

Regular mobile page

LCP3.8s
CLS0.18
TBT480ms
TTFB1.2s
FCP2.6s

⚡ AMP page (from Search)

LCP0.9s
CLS0.01
TBT0ms
TTFB~0ms
FCP0.6s

The honest verdict

Working against it

  • The SEO incentive is gone. Since 2021, regular pages meeting Core Web Vitals qualify for Top Stories — AMP is no longer required.
  • Pages serve from google.com. The URL is google.com/amp/yoursite.com; users never see your domain, hurting brand trust and breaking analytics attribution.
  • Severe DX cost. No custom JS, no standard CSS animations, a custom component library to learn — many interactive patterns are painful or impossible.

Still in its favour

  • Great for editorial / news. Purely informational articles and landing pages build in AMP with minimal sacrifice, and the gains are real.
  • The ideas are now standard. No render-blocking resources, pre-sized media, inlined critical CSS — exactly what Lighthouse and CWV push you toward anyway. AMP made them mainstream.

The bottom line

"Optimise for what the user perceives: TTFB and FCP gate the start, LCP marks the main content, and INP and CLS govern how it behaves once it's there. The three Core Web Vitals — LCP, CLS, INP — are the ones Google ranks on. For a new project, hit those thresholds natively with good engineering rather than reaching for AMP; the performance delta is smaller than it used to be, and AMP only ever made sense when it was the one reliable path to the Search carousel — which it no longer is."

Five follow-ups you should be ready for

  1. Why did INP replace FID? FID only measured the delay before the first interaction's handler ran. INP measures full interaction latency (input delay + processing + presentation) across the whole page life, at the 98th percentile.
  2. Why is TBT lab-only? It needs the full set of long tasks between FCP and TTI, available in synthetic runs (Lighthouse) but not from real users — where INP is collected instead.
  3. What inflates CLS the most? Images and ad/embed slots without reserved space. Set dimensions or aspect-ratio and min-height on containers.
  4. Can frontend work fix TTFB? No — it's a server/network metric. Edge caching, DB tuning, and 103 Early Hints are the levers.
  5. Is AMP still worth adopting? Rarely for new projects. Hit Core Web Vitals natively; reserve AMP for static editorial content where the constraints don't hurt.
Performance metrics field guide · Frontend Field Guides

Before you leave — how confident are you with this?

Your honest rating shapes when you'll see this again. No grades, no shame.

Comments

to join the discussion.

Loading comments…

Keep reading