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