Skip to content
Performance & Core Web Vitals

Cross-cutting Quality

Listen 0%
Speed

15 · Performance & Core Web Vitals

Measuring and improving real-user performance: the Core Web Vitals (LCP, INP, CLS), the metrics around them, profiling, network and runtime optimization, and bundle budgets. Current as of INP as a Core Web Vital (it replaced FID in 2024) and the web-vitals v4+ era.


Positioning

Performance is a feature with revenue and SEO consequences: Core Web Vitals are a Google ranking signal, and latency correlates directly with conversion and bounce. A senior engineer treats performance as a measured, budgeted, continuously-monitored property, not a one-off optimization sprint. The discipline: measure real users (field data), diagnose with lab tools, fix the highest-impact bottleneck, and put a budget in CI so it doesn’t regress.


Foundations: the metrics

Core Web Vitals (the three Google ranks on)

  • LCP — Largest Contentful Paint (loading): time until the largest content element (hero image, headline) is rendered. Good ≤ 2.5s. Driven by TTFB, render-blocking resources, resource load time, and client-side render delay.
  • INP — Interaction to Next Paint (interactivity): the responsiveness metric that replaced FID in March 2024. Measures the latency of (nearly) all interactions across the page lifecycle — from input to the next paint reflecting it. Good ≤ 200ms. Driven by long tasks blocking the main thread, heavy event handlers, and large re-renders.
  • CLS — Cumulative Layout Shift (visual stability): how much visible content shifts unexpectedly. Good ≤ 0.1. Driven by images/ads/embeds without dimensions, injected content, and web fonts (FOUT/FOIT).

Supporting metrics

  • TTFB (Time to First Byte) — server + network latency; the floor for LCP.
  • FCP (First Contentful Paint) — first pixel of content.
  • TBT (Total Blocking Time) — lab proxy for INP; sum of long-task blocking time.
  • Long tasks — main-thread tasks > 50ms; the enemy of INP.

Field (RUM) vs Lab data

  • Field / RUM — real users (CrUX dataset, your own web-vitals reporting). This is what Google ranks on and what reflects reality (devices, networks, geography). INP/LCP/CLS are field metrics.
  • Lab — synthetic, reproducible (Lighthouse, WebPageTest, DevTools). Great for diagnosing and CI gating, but can’t fully predict field INP. You need both: field to know you have a problem, lab to fix it.

Deep dive

1. Measuring: the web-vitals library + PerformanceObserver

The Google web-vitals library (onLCP, onINP, onCLS, onTTFB, onFCP) reads the underlying Performance APIs and reports field values; send them to your analytics/RUM endpoint via navigator.sendBeacon. Under the hood these use PerformanceObserver entry types (largest-contentful-paint, layout-shift, event, longtask, long-animation-frame). The newer LoAF (Long Animation Frames) API gives much better INP attribution (which script/handler caused the slow frame).

import { onLCP, onINP, onCLS } from 'web-vitals';
const send = (m) => navigator.sendBeacon('/rum', JSON.stringify(m));
onLCP(send); onINP(send); onCLS(send);

2. Improving LCP

  • Cut TTFB: CDN, edge/SSR streaming (07), caching (08, 18).
  • Remove render-blocking CSS/JS: inline critical CSS, defer non-critical JS (async/defer, 01).
  • Prioritize the LCP resource: fetchpriority="high" on the hero image, preload it, ensure the preload scanner can find it (don’t inject it via JS).
  • Serve right-sized, modern images (AVIF/WebP, responsive srcset, next/image-style optimization).
  • Reduce client render delay: SSR/SSG the above-the-fold content so LCP isn’t gated on hydration (07).

3. Improving INP (the hard one in 2026)

  • Break up long tasks: yield to the main thread (scheduler.yield() where available, setTimeout/postTask, chunked work).
  • Move heavy work off the main thread: Web Workers (04) for parsing/computation.
  • Reduce JS execution: ship less JS, code-split (14), avoid hydrating everything (islands/RSC, 07).
  • Optimize re-renders: React Compiler / memoization, virtualization for long lists (TanStack Virtual), avoid synchronous layout in handlers (01 layout thrashing).
  • Use transitions (useTransition/useDeferredValue, 05) to keep input responsive while expensive updates render at lower priority.
  • Defer non-urgent work until after the interaction paints.

4. Improving CLS

  • Always set width/height (or aspect-ratio) on images, video, embeds, ads so space is reserved.
  • Reserve space for dynamically injected content (banners, skeletons sized to content).
  • Manage fonts: font-display: optional/swap + preloading + size-adjust to minimize reflow; use size-adjust/fallback metrics matching.
  • Avoid inserting content above existing content after load.
  • Use the CSS contain / content-visibility where appropriate.
  • HTTP/2/3 multiplexing, compression (Brotli), CDN edge delivery, caching headers (immutable, stale-while-revalidate).
  • Preconnect/dns-prefetch to critical origins; preload critical assets; modulepreload for JS chunks.
  • Minimize request waterfalls (don’t over-split, 14; avoid chained dependent fetches — aggregate in a BFF/RSC, 12).

6. Runtime performance & profiling

  • Chrome DevTools Performance panel — flame charts, long tasks, layout/paint, the new Performance/Insights with LoAF attribution.
  • React Profiler / React DevTools — find expensive renders and why a component re-rendered.
  • Lighthouse / PageSpeed Insights — lab + CrUX field, prioritized opportunities.
  • WebPageTest — filmstrips, connection throttling, real devices.
  • The method: measure → find the biggest bottleneck → fix → re-measure. Don’t optimize on intuition.

7. Budgets & guardrails (making it stick)

  • Performance budgets: max bundle size per route, max LCP/INP/CLS thresholds. Enforce in CI (Lighthouse CI, bundlesize, size-limit, the bundler analyzer from 14).
  • Continuous RUM monitoring with alerting on CWV regressions per release.

Worked example: fixing a janky filter interaction (INP)

// BEFORE: typing filters a 10k-row list synchronously → long tasks → INP > 500ms
function List({ rows }) {
  const [q, setQ] = useState('');
  const visible = rows.filter(r => r.name.includes(q)); // blocks the keystroke paint
  return <>{/* ...render thousands of rows... */}</>;
}

// AFTER: keep input responsive + virtualize the list
function List({ rows }) {
  const [q, setQ] = useState('');
  const deferredQ = useDeferredValue(q);                 // expensive filter at low priority
  const visible = useMemo(() => rows.filter(r => r.name.includes(deferredQ)), [rows, deferredQ]);
  // + TanStack Virtual so only ~20 rows are in the DOM
  return <VirtualizedRows items={visible} />;
}

The keystroke paints immediately (high priority); the filtered list catches up without blocking input. Virtualization keeps the DOM small so each update is cheap. Measure INP before/after in the field to confirm.


Pitfalls & gotchas

  • Optimizing lab scores while field INP/LCP stay bad — real devices/networks differ; trust field data.
  • Chasing FID — it’s gone; INP is the interactivity metric now.
  • Memoizing everything to fix INP — often the wrong lever; long tasks and over-rendering are usually the cause (and React Compiler handles memo, 05).
  • Images without dimensions — the most common CLS cause.
  • Hydrating the whole page — ships and executes JS that tanks INP; prefer islands/RSC (07).
  • Request waterfalls from chained fetches or over-splitting (14).
  • No budget in CI — performance silently regresses release by release.
  • Layout thrashing in handlers (read-then-write-then-read of layout, 01).

Interview questions

  1. Name the three Core Web Vitals, what they measure, and “good” thresholds.
  2. What replaced FID, and why is INP harder to optimize?
  3. Field vs lab data — why do you need both?
  4. How would you diagnose and fix a slow LCP? A bad INP? High CLS?
  5. What causes long tasks and how do you break them up?
  6. How do useTransition/useDeferredValue help responsiveness?
  7. How does rendering strategy (CSR/SSR/RSC/islands) affect CWV?
  8. What’s a performance budget and how do you enforce it?
  9. What’s the LoAF API and why does it matter for INP?
  10. How do images and fonts cause layout shift, and how do you prevent it?

Recommendations

  • Measure field CWV with web-vitals → RUM; gate lab CWV + bundle size in CI (Lighthouse CI / size-limit).
  • Treat INP as the 2026 priority: less JS, fewer long tasks, off-main-thread work, transitions, virtualization.
  • Protect LCP with SSR/streaming above-the-fold, prioritized hero image, fast TTFB.
  • Kill CLS with dimensions/aspect-ratio on media and reserved space for injected content + font strategy.
  • Set and defend performance budgets; alert on per-release regressions.
  • Profile with DevTools + React Profiler; fix the biggest bottleneck, then re-measure.

Books & references

  • web.dev/vitals and web.dev/learn/performance — Google’s authoritative, current CWV guidance (the source of truth).
  • web-vitals library (github.com/GoogleChrome/web-vitals) — measure field metrics correctly.
  • “High Performance Browser Networking” — Ilya Grigorik (free at hpbn.co). The networking foundation (18).
  • Addy Osmani’s writing (addyosmani.com, “Image Optimization”) — practical perf, image, and JS-cost guidance.
  • Lighthouse / PageSpeed Insights / WebPageTest docs — the lab tooling.
  • “Designing for Performance” — Lara Hogan. Budgets and the perf-culture side.
  • Chrome DevTools Performance docs — flame charts, LoAF, Insights.

Connections

Frontend Deep-Dive Library · content is the single source of truth.