The rendering decision is usually presented as a performance question and is actually a product type question. A content site lives or dies by the first paint and the crawler, a trading dashboard lives or dies by the speed of its interaction loop, and neither one's requirements transfer to the other. Server side rendering, client side rendering, and the static generation option beside them, are answers to different products, and the teams that choose by fashion end up paying the costs of the wrong answer on both sides: the SSR application that carries a server fleet for a logged in tool nobody crawls, and the CSR application that shipped an empty div to every search engine on the internet. This article draws the decision honestly: what each rendering mode actually does, what it costs, the static third option that dominates more cases than teams expect, and the per product type mapping that turns the decision from a debate into a lookup.
What each rendering mode actually produces
Client side rendering ships a minimal HTML document, a script bundle, and an empty mount node. The browser downloads the application, executes it, the application fetches data, and the page assembles on the user's device. The first meaningful paint waits for the entire chain: script download, parse, execute, data fetch, render. What the server sends is small and cacheable, and after the first load the application behaves like a local program, routing without round trips and updating the DOM in place.
Server side rendering runs the same component code on the server for each request and sends complete HTML. The browser paints text and layout immediately, the script then hydrates the page, attaching event listeners and state without a visible flash, and the page becomes interactive. The first paint stops waiting on the application chain, but the server now does per request work, a render pass of real components, and the hydration pass still costs the client its download and execution before the page responds to clicks.
Static generation renders at build time. The build runs the components once per route, writes the HTML to files, and the serving layer is a content delivery network handing out prebuilt pages. The client may still hydrate for interactivity, but the server does zero per request rendering. The trade is that the content is fixed at build time, and routes that must reflect per request data, the account page, the cart, fall back to client fetching after load, or to the incremental regeneration that modern frameworks offer, where pages rebuild on a schedule or on demand with a configurable staleness.
The costs that each mode bills monthly
The costs are asymmetric and predictable. CSR bills the user's device: the bundle download, the parse and execute time, and the empty paint window, which on mid range phones and average networks is measured in seconds. The product's perceived speed after that investment is excellent, routing is instant, and the application can use full browser APIs without ceremony. The bill lands on the user, which is why the CSR failure is invisible in the office and catastrophic in the field.
SSR bills the infrastructure: every page view is a server render, CPU bound work on the data fetching path, which means a server fleet, load aware routing, cache keys per user segment, and the operational surface of any stateful tier, memory pressure, cold starts, deploys that warm up. The second bill is complexity on the data path: the server needs the data before render, which means the fetching layer works in two runtimes with two sets of APIs, the waterfalls that a client would pipeline have to be explicitly parallelized on the server, and the caching discipline, which segments, which TTLs, which invalidation, becomes a standing engineering conversation. The classic SSR defect, the hydration mismatch where server and client render different markup for the same state, is the tax on the component contract, and every framework's documentation has a long page on its causes.
Static generation bills the build: the build is long, minutes to hours for large sites, and the content's freshness is the build's cadence. The operational profile is the best of the three, serving from a CDN at edge speeds, but the product must tolerate the staleness model, and the escape hatches to per request data exist precisely because most products have a handful of routes that are genuinely per user, sitting inside an otherwise static site.
The per product type mapping
The decision is a lookup once the product type is named honestly. Content sites, marketing pages, publications, documentation and blogs, are static generation territory by default: crawled, shared, linked, read from any device on any network, and updated at human cadence. The interactive exception routes, search, user specific pages, hydrate or fetch client side inside the static shell. The incremental regeneration option covers the news and catalog cases where build time staleness is too stale, rebuilding on a timer or on demand.
Commerce at scale sits in the middle and mostly lands on SSR with aggressive caching. The product page is the money page, crawled, shared and judged by its first paint on a phone, while prices, stock and recommendations are per request data, and the SSR cache becomes the architecture: the shared segments cached at the edge, the personal segments streamed in as fragments. The account area, the checkout and the logged in tools behind those pages, are CSR within the SSR shell, because after authentication the crawler is irrelevant and the interaction speed is everything.
Dashboards, editors, internal tools and any application behind a login are CSR's home ground. Nobody crawls the logistics planner, the first visitor does not need a painted shell before the bundle arrives because the bundle is the product, and the complexity budget is better spent on the interaction loop than on a server fleet. The SEO argument that so often drags logged in products toward SSR is answered by one question, how do users arrive, and if the answer is through a login page, the crawler never sees the application anyway.
Streaming islands and the newer escape from the either or
The rendering discussion has moved past the binary in the last generation of frameworks, and the new mechanisms are worth naming because they dissolve some of the older costs. Streaming server rendering splits the response: the shell arrives painted, and slower data sections arrive as they complete, filling placeholders the shell reserved, so one slow widget no longer delays the whole page's first paint. The server components model moves the boundary inside the component tree, components that never need the browser, data assembly and pure rendering, execute on the server and ship no JavaScript at all, while interactive islands keep their client bundle, and the payload per route shrinks to the interactive surface only.
The islands architecture, from the static site side, is the same idea inverted: the page is static HTML, and individual widgets, the cart, the search box, the comment form, hydrate independently as small applications. The product page keeps its instant edge served paint, the widget becomes interactive a moment later on its own schedule, and the main thread never runs a full page hydration at all. These mechanisms do not change the decision table above, they change its costs: the SSR route stops paying the full bundle for its static parts, the static route gains interactive surfaces without gaining a monolithic hydration, and the team evaluating a framework in any of the families should ask which of these levers it exposes, because a lever that matches the product's split, mostly static with interactive spots, is worth more than a benchmark delta.
The hybrid reality that most products converge on
The production answer for most real products is a composition, not a choice. The marketing and content routes render statically, the money routes that strangers visit render on the server with caching, the application routes behind auth render on the client, and the routing layer already supports this split, because every mainstream framework treats the rendering mode as a per route decision. The mistake the composition prevents is the uniform one, SSR everywhere, paying the server fleet for the admin tool, or CSR everywhere, shipping empty shells to the crawler that indexes the shop.
The composition has one standing discipline: the boundaries are written down. The team knows which routes are static, which are server rendered, which are client, and the route's rendering mode changes in a review, not in a quiet refactor, because the choice has costs, bundle structure, data fetching contracts and caching keys, that other teams depend on. A rendering mode drift discovered in an incident review is the signature of a codebase that stopped making the decision on purpose.
The per route decision written in code
The decision becomes concrete when the route table states it, and modern frameworks make the statement declarative. The same product with a content page, a money page and a tool, in one framework's file conventions:
// app/blog/[slug]/page.js -> statically rendered at build
export const revalidate = 3600
// app/product/[sku]/page.js -> server rendered with caching
export const dynamic = 'force-dynamic'
// app/planner/page.js -> client rendered application
'use client'
import Planner from './Planner'
export default function Page() { return <Planner /> }
Three routes, three modes, one repository, and the rendering map this article asked for exists as readable configuration instead of tribal knowledge. The revalidate declaration gives the blog hourly freshness without a render fleet, the product page keeps its server render for the crawler and the money paint, and the planner declares its client boundary with the directive every reviewer can see. The pattern generalizes across frameworks with different syntax, and the review conversation it enables is the right one, why is this route in this mode, asked per change, instead of the architecture wide shouting match it replaces.
The decision checklist that survives the debate
The debate ends with questions answered in advance, and the working list is short:
- Name how strangers arrive and what the crawler must see, per route, not per application;
- Classify each route as content, money or tool, and let the class pick the rendering mode;
- Budget the field first paint for the worst device you support, and measure it there;
- Count the server render load and the cache keys honestly before choosing SSR for anything that is not crawled;
- Prefer static generation with hydration for everything that can be build time, and use incremental regeneration before reaching for a render fleet;
- Write the per route rendering map into the repository, and change it through review, not drift.
The rendering modes are tools with different cost profiles, not ideologies with different tribes, and the product's arrival pattern decides which tool each route deserves. The team that classifies its routes honestly usually discovers that its application wanted a composition all along, and that the debate it inherited was between two answers to two different questions that its product was asking simultaneously.
The data layer and the double fetch problem
The rendering mode decides where data is fetched, and the SSR choice introduces the problem that gives every framework its data fetching library. A client rendered page fetches once, on the device. A server rendered page must fetch on the server for the paint, and the client, after hydration, must not fetch the same data again, so the server render serializes the fetched state into the response and the hydration reuses it, the dehydrate and rehydrate pair. The serialization is the sharp edge, dates arrive as strings and must parse back to dates, maps and sets degrade to plain structures, and a mismatch between what the server rendered and what the client reconstructs is the hydration warning every SSR team meets in its first month.
The modern frameworks absorb the pair into their data libraries, server components fetching with server only credentials and streaming their results, the client loaders caching per route, and the application level discipline is the boundary rule: fetching code that needs secrets or database access lives where those are available, fetching code that needs user interactivity lives where the events are, and the two never share a module boundary by accident. The double fetch problem is why SSR applications have a data layer at all, and the layer's quality, its cache keys, its invalidation and its serialization, is the difference between SSR that is faster in the field and SSR that is the same page fetched twice with extra machinery.