Library
Learning Patterns · 8 of 12
Learning Patterns
UI/UX Design HIGH

Web Performance Metrics and Measurement

Learning Patterns Lydia Hallie & Addy Osmani
core-web-vitals lighthouse performance-measurement case-study

Key Principle

Every rendering strategy is a tradeoff across a fixed set of performance metrics, not a universal upgrade. JS bundle size is the single most impactful lever because it degrades FCP, LCP, TTI, and TBT simultaneously. The discipline of measuring before and after each change -- isolating one variable at a time -- is essential because some "optimizations" cause regressions on axes you did not expect.

Why This Matters

Core Web Vitals are interdependent: smaller bundles reduce TBT, which unblocks TTI, which lowers perceived LCP. This means cumulative micro-optimizations compound nonlinearly. But it also means a change that improves one metric can regress another. Without measurement at each step, you cannot distinguish a net win from a net loss. The Movies app case study demonstrates both directions -- dramatic wins and instructive failures.

KPI Reference

Metric What It Measures Primary Driver
TTFB First byte arrival Server processing time
FCP Requested content first visible JS bundle size, render strategy
LCP Largest viewport element visible JS bundle size, render strategy
TTI Page fully interactive JS bundle size, hydration cost
TBT Blocked time between FCP and TTI JS execution on main thread
CLS Visual stability / layout shift Missing dimensions, late-loading assets

Good Examples

Movies App Case Study (Hallie & Osmani, "Learning Patterns", 2022)

Overall Lighthouse score: 64% to 95.7% through incremental, measured changes.

Metric Before After
FCP 3.06 s 0.83 s
LCP 5.0 s 2.43 s
Speed Index 8.5 s 3.23 s

Key wins, each measured in isolation:

  • Replacing Font Awesome with @svgr/webpack: Speed Index -34%, LCP -23%, TBT -51%. Single largest gain from a single substitution.
  • Removing react-burger-menu + resize-observer-polyfill: 34.28 kB gzipped bundle reduction.
  • Adding rel=preconnect: LCP improved 16.61% (3.43s to 2.86s), overall 88% to 93.33%.
  • Preloading API responses (<link rel='preload' as='fetch'>): LCP -12.65%, TTI -7.76%, overall 91% to 94%. Only viable for deterministic requests.
  • Aspect-ratio CSS for images: CLS reduced from 0.016 to 0. Zero is the real target, not just passing thresholds.
  • Parallelizing sequential API calls: Zero-cost removal of artificial bottleneck.

Counterpoints

Not every optimization helped. The case study documented several regressions:

  • Native lazy-loading replaced react-lazyload: TBT spiked from 10ms to 117ms for negligible LCP gain. Native loading fetches images near the viewport; the library loaded only those within it.
  • @artsy/fresnel for responsive SSR: Speed Index improved 13.4%, but FCP regressed ~14% due to the fresnel bundle size.
  • Google Analytics addition: Regressed every metric -- TBT +31.28%, FCP +18.75%, TTI +18.05%, LCP +15.61%, overall 95.66% to 92.75%. An accepted tradeoff for engagement data.
  • Full SSR with external API dependencies: Regressed performance because all fetch/response processing moved server-side, slowing the initial response.

Key Quotes

  • "The decision on how and where to fetch and render content is key to the performance of an application." (Introduction, Hallie & Osmani)
  • "The Chrome team has encouraged developers to consider static rendering or server-side rendering over a full rehydration approach." (Introduction, Hallie & Osmani)
  • "If the preloaded data is not used, it will be a waste of resources resulting in a warning." (Preloading API response section, Hallie & Osmani)
  • "It is possible that native image lazy loading loads a few images that are near the viewport while react lazy-load only loads those that are within the viewport causing this difference in TBT." (Ideas that did not help, Hallie & Osmani)

Rules of Thumb

  1. Measure after every single change. Isolate variables. Some "improvements" regress other metrics.
  2. Audit third-party packages first. A single library swap can yield the largest gain in the entire optimization effort.
  3. Bundle size cascades across metrics. Reducing JS parse/execute time improves FCP, LCP, TTI, and TBT simultaneously.
  4. Preconnect > preload for cross-origin resources. Front-loading DNS/TCP/TLS yields disproportionate LCP gains.
  5. Only preload deterministic requests. Unused preloads waste bandwidth and trigger browser warnings.
  6. Target CLS = 0, not just "passing." Passing thresholds (e.g., 0.016) still produce perceptible shifts on slow connections.
  7. Accept some regressions consciously. Analytics, responsive SSR libraries -- document the tradeoff and move on.
  8. Rendering location determines which KPIs suffer. Server rendering improves FCP/LCP but worsens TTFB. Client rendering shifts cost to TTI/TBT.

Related References

  • Rendering Patterns spectrum: CSR, SSR, SSG, iSSG, Islands Architecture
  • React 18 streaming SSR and selective hydration (decouples per-component readiness from whole-page readiness)
  • PRPL pattern for HTTP/2 optimized delivery
  • List virtualization for large dataset rendering performance