Key Principle
Every rendering pattern is a trade-off across performance KPIs, not an upgrade over another. The rendering location (server vs. client) and timing (build-time vs. request-time vs. runtime) determine which metrics improve and which degrade. Choosing a pattern means choosing which KPI to prioritize.
Why This Matters
JS bundle size is the single dominant performance lever -- it degrades FCP, LCP, TTI, and TBT simultaneously. The entire rendering spectrum exists to manage where and when that JS executes. Picking the wrong pattern for a use case creates structural performance debt that no amount of optimization can fully resolve.
Good Examples
Pattern-to-KPI Map:
| Pattern | TTFB | FCP/LCP | TTI | Best Use Case |
|---|---|---|---|---|
| CSR | Low (empty shell) | Poor (blocked by JS) | Poor (TTI >>> FCP) | High-interactivity SPAs where SEO is secondary |
| Classic SSR | High (per-request processing) | Good (pre-rendered HTML) | Good (FCP = TTI, minimal JS) | Static content: news, encyclopedias |
| SSR + Hydration | High | Good | Degraded (TTI > FCP) | Content-first pages needing client interactivity |
| Streaming SSR | Low, consistent | Good (chunked flush) | TTI > FCP | Pages decomposable into independent chunks (search results) |
| SSG | Lowest (pre-built, CDN-cached) | Excellent | Excellent (FCP ~ TTI) | Infrequently changing content (About, Contact, blogs) |
| iSSG | Low (cached static files) | Excellent | Excellent | High-volume content with moderate change frequency (product listings) |
| Progressive Hydration | Varies | Good | Improved (incremental JS) | Interactive pages where some components can defer activation |
TTI:FCP Relationship as Decision Heuristic:
- TTI = FCP: Classic SSR, SSG, iSSG -- page is interactive as soon as it paints.
- TTI > FCP: SSR with hydration, Streaming SSR, Progressive Hydration -- user sees content before JS finishes attaching handlers.
- TTI >>> FCP: Full CSR -- empty shell renders instantly but full JS bundle must execute for any interactivity.
Next.js Per-Page Strategy Selection:
getStaticPropstriggers Static Generation (build-time fetch).getStaticPathsenables SSG for dynamic routes.getServerSidePropstriggers SSR (per-request fetch).- This allows mixing patterns within one app: marketing pages use SSG, dashboards use SSR or CSR.
Counterpoints
- CSR with optimizations (code splitting, lazy loading, JS budgeting, preloading, app shell caching) can be sufficient: "sometimes it may be enough to use client-side rendering with some tweaks instead of going for a completely different pattern." The choice depends on whether the bottleneck is initial load (favors SSR/SSG) or interaction responsiveness (favors CSR).
- SSG breaks down at scale due to three coupled limits: rebuild coupling (any edit forces full rebuild), hosting dependency (performance requires CDN edge-caching), and content staleness (pages frozen at build time).
- SSR degrades TTFB under load because every request is processed from scratch -- "even if the output of two consecutive requests is not very different, the server will process and generate it from scratch."
- iSSG uses stale-while-revalidate, meaning users may briefly see outdated content during background regeneration.
Key Quotes (with citations)
- "The decision on how and where to fetch and render content is key to the performance of an application." (Introduction)
- "The Chrome team has encouraged developers to consider static rendering or server-side rendering over a full rehydration approach." (Introduction)
- "As the size of bundle.js increases, the FCP and TTI are pushed forward. This implies that the user will see a blank screen for the entire duration between FP and FCP." (JavaScript bundles and Performance section)
- "Lesser JavaScript leads to quicker FCP and TTI [...] FCP = TTI. With SSR, users will not be left waiting for all the screen elements to appear and for it to become interactive." (SSR, p. 2)
- "iSSG uses the stale-while-revalidate strategy where the user receives the cached or stale version while the revalidation takes place. The revalidation takes place completely in the background without the need for a full rebuild." (iSSG, p. 3)
- "These variations use a combination of techniques to lower one or more of the performance parameters like TTFB (Static and Incremental Static Generation), TTI (Progressive Hydration) and FCP/FP (Streaming)." (Conclusion, p. 2)
Rules of Thumb
- Start with static. If content does not change per-user or per-request, SSG or iSSG gives the best KPI profile across the board.
- Budget JS at 100-170KB min+gzip. Beyond this threshold, FCP and TTI degrade measurably on CSR pages.
- Use the TTI:FCP gap as a diagnostic. A large gap signals excessive hydration cost or bundle size.
- SSR and CSR disadvantages are mirror images. SSR: fast initial load, slow interactions. CSR: slow initial load, fast interactions. Pick based on the dominant user journey.
- iSSG over SSG when page count is high or content updates moderately. It eliminates full rebuilds via per-page background regeneration (available since Next.js 9.5, granularity as low as 1 second).
- Code splitting is non-negotiable for CSR. Route-based splitting should be the default; component-based splitting via
dynamic import()handles heavy components. - iSSG failure is graceful. "Even if the regeneration fails in the background, the old version remains unaltered" -- users always see a page.
Related References
- CSR optimization techniques (JS budgeting, preloading, lazy loading, code splitting, app shell caching)
- Hydration mechanics:
renderToString()on server,hydrate()on client - Next.js data-fetching API:
getStaticProps,getStaticPaths,getServerSideProps - Progressive Hydration and Streaming SSR as TTI/FCP optimizations
- Web Vitals KPI definitions: TTFB, FCP, LCP, TTI, TBT