StrategiesGENERALFRAMEWORK-SPECIFIC

Choosing a Rendering Strategy

There is no winner. There are seven questions, and a real application usually answers them differently for different routes.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

Which rendering strategy should this route use, and what am I actually deciding when I pick one?

The user intent

A team is about to build something, and someone has to answer "so are we doing server rendering or not" before anyone can start.

The obvious build

Pick one strategy for the application. It keeps the codebase coherent, the deployment simple and the mental model uniform — and the framework has a default, which is presumably the right answer for most cases.

Why it breaks

The routes have different requirements. A marketing page, a login screen, a documentation article and an internal dashboard have nothing in common in indexability, personalization or freshness, and one strategy will be wrong for at least two of them.

How it breaks in a real browser
  • The routes have different requirements. A marketing page, a login screen, a documentation article and an internal dashboard have nothing in common in indexability, personalization or freshness, and one strategy will be wrong for at least two of them.
  • A global choice optimises the wrong thing. The application-wide decision is usually made from the route the team works on most, which is rarely the route most visitors see first.
  • The framework default encodes the framework author's assumptions about typical applications, not a measurement of yours (Choosing a Framework).
  • The costs are paid in different currencies — server spend, build time, bundle bytes, staleness, complexity — so "which is faster" is not a question with an answer until you say for whom, on what device, and measuring what.
  • Choosing once means never revisiting it. Traffic patterns, page counts and personalization requirements all change, and a decision nobody wrote down cannot be reconsidered.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • Every strategy in this module is a position on two axes: where the HTML is produced — build machine, request-time server, or browser — and how much JavaScript must run before the page responds (Client-Side Rendering, Server-Side Rendering, Static Site Generation).
  • The first axis decides when content is readable and who can read it without executing script. The second decides the gap between readable and usable (Hydration).
  • The two axes are independent. Static generation with a full application bundle has the same interactivity gap as per-request rendering; islands with a client-rendered shell has almost none. This independence is what makes a single-word answer inadequate (Islands and Partial Hydration).
  • Streaming is a modifier on the first axis rather than a separate point: it changes when the bytes leave rather than where they were produced (Streaming Server Rendering).
  • Server components are a modifier on the second axis for the frameworks that have them: they change how much of the tree is client code at all (Server Components).
  • Because they are modifiers rather than alternatives, they compose — and because they compose, the useful unit of decision is the route, not the application.

What this makes the browser do

And which of it is avoidable.

  • The strategy decides how much of the document arrives ready to parse and how much has to be constructed by script (Tree Construction).
  • It decides whether the bundle is on the critical path to first content or only to interactivity (The Critical Rendering Path).
  • It decides how much of the tree gets walked to attach behaviour, which on a content-heavy page is most of the cost (Hydration).
  • It does not decide how much JavaScript your route imports, how expensive your components are, or how many DOM nodes you render. Those are separate levers, and on many pages they matter more than the strategy does (The Real Cost of JavaScript).

The questions, in the order that actually decides it

The order matters because the first question that comes back "yes" eliminates most of the options, and the later questions only rank what remains. Asking about server cost first, which is what usually happens, produces a decision that has to be reversed as soon as somebody mentions search.

Answer these per route. A single application will produce different answers for its landing page, its documentation, its sign-up flow and its dashboard, and that is the correct outcome rather than an inconsistency to be cleaned up.

Choosing a strategy for one route

What does this specific route have to be able to do?

Does anything need to read it without executing JavaScript?

when Search engines, link previews, feed readers, text browsers, or simply a user whose bundle failed to load. If yes, the content must be in the HTML — which rules out client rendering for this route and nothing else.

cost Producing that HTML means either a build step that must enumerate the route, or a server that renders it per request. Both are real infrastructure that a static shell did not need (Client-Side Rendering).

Is the first view different per user?

when A name, a permission, a cart, a price in their currency, a tenant. If yes, the response cannot be built once for everyone and cannot be cached by a shared cache.

cost Per-request rendering, private cache directives, and a serialization boundary to review on every response. The alternative — a shared document personalized after paint — costs a round trip, reserved space and an announcement (Server-Side Rendering).

How fresh must it be?

when Stale by hours is fine for documentation; stale by minutes is not acceptable for a price or an inventory count.

cost Freshness is bought either with per-request rendering or with build frequency. The middle position — generate, then top up the volatile values on the client — costs a bundle and a pending state (Static Site Generation).

How much of the page is genuinely interactive?

when An article with a search box is content with two islands. A spreadsheet is one island the size of the page.

cost Islands and server components reduce what ships and add a boundary the component model must carry, plus explicit plumbing for anything two regions share (Islands and Partial Hydration).

How many pages are there, and can you enumerate them?

when Hundreds is a build. Hundreds of thousands, or an unbounded parameter space, is not — at least not exhaustively.

cost Beyond what a build can hold, the options are on-demand generation with a cache, or per-request rendering. Both mean operating a server for a route you wanted to be a file (Static Site Generation).

What can you afford to run, and what happens under a spike?

when Per-request rendering turns views into origin CPU; a cached file does not care how many people arrive.

cost Cheaper serving means less capability: no personalization in the document, and freshness bounded by how often you build (CDN Delivery).

Is one data source much slower than the rest?

when A dashboard with one slow report, or a page whose sidebar calls a third party.

cost Streaming fixes exactly this and makes error handling after the first flush genuinely harder, because the status code has already been sent (Streaming Server Rendering).

The strategies against the criteria

Read the last two columns together and separately from the rest. Content visible and Interactive are the two milestones this whole module is about, and the single most common mistake in this decision is assuming that a strategy which improves the first must improve the second.

Nothing in this table is a winner. Every row is good at something and bad at something else, and the row that is right for a route is the one whose weaknesses that route does not care about.

StrategyReadable without JavaScriptCan be personalFreshnessCost to serveContent visibleInteractive
Client-Side RenderingNo — the document is a shellYes, after the bundle runsLive, every timeLowest: a file from an edgeLatest of the fiveTogether with content, because there is nothing to hydrate
Server-Side RenderingYesYesLive, per requestHighest: CPU per viewEarlyLatest — the widest gap in the module
Static Site GenerationYesNo, not in the documentAs of the last buildLowest: a file from an edgeEarliest of the fiveUnchanged from server rendering, and more visible
Streaming Server RenderingYes, progressivelyYesLive, per regionSame as per-request rendering, spread over timeEarly, in piecesPer region, so a single figure misrepresents it
Islands and Partial HydrationYes — the static majority is real HTMLDepends on the underlying strategyDepends on the underlying strategyDepends on the underlying strategyAs early as its underlying strategyEarliest of the five, per island
Server ComponentsYesYesLive, per request or per buildPer-request server work, plus a round trip per navigationEarlyNarrowed for what remains; unchanged for each client component

One application, several strategies

The conclusion this module has been building towards is that the interesting artifact is not a choice but a table. Real applications mix, because their routes have genuinely different requirements — and the mixing is not a compromise or a sign of an unfinished migration. It is the decision being made at the right granularity.

The cost of mixing is real and it is smaller than it looks: more than one shape to understand, and a discipline of recording which route is which. The cost of not mixing is that every route is optimised for whichever one the decision was made from.

How the decision gets recorded
One answer for the whole application
"We're an SSR app."

  /                 server-rendered per request
  /docs/*           server-rendered per request   <- identical for everyone,
                                                     rebuilt on every view
  /login            server-rendered per request   <- nothing to render
  /app/*            server-rendered per request   <- behind auth, unindexable,
                                                     personal, long session
  /pricing          server-rendered per request   <- changes twice a year

Consequences: origin CPU scales with every documentation reader; the
dashboard pays a per-request render for a page no crawler will ever see;
and nobody can say why, because the decision predates everyone.
One answer per route, written down
// Strategy is a property of the route. One line each, with the reason.

  /            SSG + islands   public, identical for all, two widgets
  /docs/*      SSG + islands   ~400 pages, rebuilt on publish, search island
  /pricing     SSG             changes twice a year; must be indexable
  /blog/[slug] SSG             enumerable at build; islands for comments
  /login       SSG             a form; the browser already implements it
  /app/*       CSR             behind auth, personal, unindexable, long session
  /app/report  Streaming SSR   one slow query; stream the shell, defer the panel
  /r/[token]   SSR             per-request, private cache, expiring share links

// Recorded next to each route, so the next person can ask whether the
// reason still holds rather than guessing what somebody meant.

The second version is not more complex to operate — most of these routes are files on a CDN, which is fewer moving parts than the first version, not more. What it adds is a reason per route, which is the only thing that makes the decision reviewable when the page count doubles or a public page acquires a personalized header. The first version has one fact and no reasons, so the only way to change it is to change everything (Deploying a Frontend).

How to build it

Most important first.

  • Decide per route, and write the decision down next to the route. A one-line comment naming the strategy and the reason survives the person who chose it (Deploying a Frontend).
  • Start from the hardest constraint rather than from a preference: if the route must be indexable, it must render content without script; if it is personal, it cannot be publicly cached; if it changes constantly, it cannot be built ahead of time.
  • Answer "who reads this without running JavaScript" before anything else. It is the only question in the list with a hard yes-or-no consequence (The Head: Metadata That Changes Rendering).
  • Separate the content question from the interactivity question and answer both. Most bad outcomes in this module come from a team answering only the first and assuming the second followed (Hydration).
  • Prefer the simplest strategy that meets the constraints. Every step towards a per-request render buys capability and costs infrastructure, cacheability and a failure mode (Static Site Generation).
  • Measure the route on a slow device and a slow connection before and after. A strategy change is a large, disruptive refactor to make on a hunch (Measure Before Optimising).
  • Revisit when the inputs change: page count crossing what a build can handle, personalization arriving on a public page, or a content route acquiring an application inside it.

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • This decision has a direct accessibility consequence and it is easy to state: content rendered before JavaScript is available to assistive technology at first paint, and content rendered by script is not available until the script has run (The Accessibility Tree).
  • A blank page during load is not a neutral state. It is a document with no headings, no landmarks and no text, and nothing announces that more is coming (Client-Side Rendering).
  • A page that looks interactive before it is accepts keyboard and screen-reader activation that goes nowhere, with no feedback. Choosing a strategy with an early paint means taking on that obligation deliberately (Hydration).
  • Every strategy here that keeps the user on one document makes route changes silent: no document load, so no announcement. That has to be handled by the router regardless of which strategy produced the first page (Focus Management).
  • Any strategy that fills content in after paint owes reserved space and, where the change is meaningful, an announcement (Live Regions and Announcement).
  • None of this is downstream of the choice being made well. A statically generated page of unlabelled divs is inaccessible very efficiently (Semantics Are Behaviour).

What can go wrong

Failure modes
  • Choosing for the team's favourite route and shipping it everywhere, so the highest-traffic entry point gets a strategy chosen for a page nobody lands on.
  • Adopting per-request rendering for indexability and then caching the responses to control cost, arriving at static generation with more moving parts and a leak risk (Server-Side Rendering).
  • Adopting a strategy for its early paint and never looking at interactivity, so the page gets faster to look at and no faster to use (Hydration).
  • Mixing strategies without writing down which route uses which, so a later change silently moves a route from cacheable to per-request.
  • The mitigation failing: splitting a page into a statically generated shell and client-fetched personal parts, and shipping so much code to fetch them that the split costs more than it saved (Bundle Analysis).
  • Treating a strategy change as a fix for a bundle problem. If the page ships too much JavaScript, every strategy here ships too much JavaScript (Code Splitting).
What can arrive out of order
  • A route that changes classification mid-project — a public page acquiring personalization — leaves cache directives written for the old classification, which is a leak rather than a slowdown (Browser HTTP Caching).
  • A deploy that changes strategy for a route while users hold documents produced by the old one, so their next navigation requests something that no longer exists in the shape they expect (Long-Lived Clients and Version Skew).
Security
  • Personalization is the pivot. The moment a route's output depends on who asked, it cannot be cached by a shared cache, and getting that wrong serves one user another user's page (Browser HTTP Caching).
  • Every per-request render happens in a process holding credentials, so every one of them has a serialization boundary where server-only data can leak into a public response (Server-Side Rendering).
  • Static generation removes that boundary at request time and moves it into the build, where the same review applies to whatever the build embedded (Static Site Generation).
  • No strategy here changes who enforces authorization. The server does, at the endpoint, every time — rendered UI is a reflection of a decision, never the decision (Authorization-Aware UI).
  • Per-request rendering makes unauthenticated page views cost origin work, which turns rate limiting into a rendering-strategy consideration (Deploying a Frontend).
Misreads
  • "Server rendering is the safe default because it is faster." It is not faster in general; it moves work. It gets content on screen before JavaScript runs and pays with per-request cost, a first byte that waits on your data, and an unchanged interactivity gap (Server-Side Rendering).
  • "A single-page application is the modern architecture." Age is not an argument. A multi-page application with a few islands is newer than most single-page architectures and is the right answer for a large class of sites (MPA vs SPA).
  • "Pick one and be consistent." Consistency in tooling is valuable; consistency in strategy across routes with different requirements is how a documentation page ends up rendering per request and a personal dashboard ends up statically generated.
  • "Static means it cannot be dynamic." It means the document was built ahead of time. What happens after paint is a separate decision (Static Site Generation).
  • "The newest option is the best one." Server components and islands solve specific problems well and add real constraints. A page that is interactive everywhere gains nothing from either (Server Components).
  • "This decision fixes performance." It decides when content arrives and how much of the tree hydrates. If the route imports too much code, every strategy here delivers too much code (Measure Before Optimising).

Measuring it, and what changes in the field

How you would see this
  • For each candidate route: when content becomes readable, when the page answers input, and how far apart those are. The gap is the number that distinguishes the strategies (Interaction Responsiveness).
  • What a non-executing client receives. Load the route with JavaScript disabled and read what is there — it is what crawlers, previews and failed-bundle users get (A Mental Model of the Devtools).
  • Origin cost per thousand views, which is the axis that decides between per-request and build-time rendering once the frontend questions are settled (CDN Delivery).
  • Build duration against page count over time, which is the constraint that quietly ends a static strategy (Deploying a Frontend).
  • Field data segmented by device class. The strategies differ most on the devices least represented in a development team (Real User Monitoring).
Slow device, slow network, large data, old tab
  • On a slow device, anything that puts content in the HTML wins on readability and nothing changes the cost of the bundle. The two effects point in opposite directions and must be weighed separately (The Real Cost of JavaScript).
  • On a slow network, the number of serial dependencies matters more than total bytes, which favours strategies that put content in the first response (Reading a Network Waterfall).
  • Under a traffic spike, a cached file costs nothing per visitor and a per-request render costs CPU per visitor. This is a capacity decision as much as a performance one.
  • At a large page count, build-time rendering stops being viable in a way that appears in the deployment pipeline rather than in any user-facing metric.
  • In a long session, the entry cost amortises and client-side navigation quality dominates — which is why applications behind a login often reasonably choose the strategy that looks worst on a first-visit chart (Client-Side Routing).
What this costs
  • Deciding per route gives every route the right shape and gives the team more than one shape to maintain, test and reason about.
  • Writing the decision down costs a sentence per route and is the only thing that makes the decision reviewable later.
  • Choosing the simplest strategy that meets the constraints means occasionally being wrong in the cheap direction, which is the right way to be wrong here — moving from static to per-request is a smaller change than the reverse.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe criteria — indexability, personalization, freshness, interactivity, build scale, server cost, cacheability — are properties of the route rather than of any framework, and they apply identically whether the implementation is a modern framework, a template engine or hand-written HTML.
  • FRAMEWORK-SPECIFICWhich strategies are available and how cheaply they mix is entirely a framework and host question: some allow a per-route choice with a one-line change, some support only one shape well, and some make streaming or islands available only on particular hosting platforms — so the decision below can be constrained by a stack choice made before anyone asked it.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Software Design: recording the strategy and the reason next to the route is an architecture decision record at the smallest useful scale, and it is the difference between a decision and an accident that nobody has questioned yet.