Rendering Architecture
How CSR, SSR, SSG, ISR, and streaming SSR actually differ at request time, what hydration really costs, and how to match a rendering strategy to the content you are shipping.
Advertisement
Why It Matters
Rendering architecture is the first decision in any frontend system design, and it constrains every decision after it. It sets your time to first byte, whether search engines can read your content, how much JavaScript the user downloads, how long the main thread is blocked before the page responds to a click, and how much you pay in server cost per request.
It is also the question interviewers use to separate people who have memorized acronyms from people who have shipped. Anyone can say "SSR is better for SEO." The useful answer explains what happens at request time, what it costs, and why a single page often needs more than one strategy at once.
The Five Strategies, Precisely
The strategies differ in one dimension: when the HTML is produced, and by whom.
Client-Side Rendering (CSR)
The server returns a near empty HTML document with a script tag. The browser downloads the bundle, executes it, the framework builds the DOM, and only then does the app fetch data and render real content.
At request time: HTML is trivial and fast, but nothing is visible until JavaScript has downloaded and run. Crawlers that do not execute JavaScript see an empty page. Every user pays the full bundle cost before first paint.
Server-Side Rendering (SSR)
On every request, the server runs the component tree, fetches the data it needs, and returns fully formed HTML. The browser paints immediately, then downloads JavaScript and hydrates.
At request time: TTFB now includes your server's render time and its data fetching. The user sees content much sooner, but the page is not interactive until hydration completes. That gap - visible but dead - is a real and frequently overlooked failure mode.
Static Site Generation (SSG)
HTML is produced once at build time and served as a file from a CDN. There is no per request server work at all.
At request time: essentially the fastest possible response, because it is a cache hit on a static asset. The constraints are that content is frozen until the next deploy, and build time scales with page count - a hundred thousand product pages is a very long build.
Incremental Static Regeneration (ISR)
SSG with a revalidation window. The page is served from cache; after the window expires, the next request triggers a background re-render, and the stale page is still served while that happens. Once the new version is ready, it replaces the cached one.
At request time: identical to SSG for the user. The trade is deliberate, bounded staleness in exchange for keeping static performance on content that changes.
Streaming SSR
The server does not wait for the whole tree to render before responding. It flushes the shell immediately, then streams the remaining chunks as their data resolves. Slow data no longer blocks fast content.
At request time: TTFB drops back to near static levels because the shell does not wait on data. The user sees layout and above-the-fold content while a slow query is still running on the server.
Diagram100%sequenceDiagram participant U as User participant S as Server / CDN participant B as Browser Note over U,B: CSR U->>S: GET /page S-->>B: Empty HTML shell B->>S: GET bundle.js S-->>B: JavaScript B->>B: Execute, build DOM B->>S: Fetch data S-->>B: JSON B->>B: Render + interactive Note over U,B: SSR U->>S: GET /page S->>S: Fetch data + render tree S-->>B: Full HTML (paint) B->>S: GET bundle.js B->>B: Hydrate, then interactive Note over U,B: SSG / ISR U->>S: GET /page S-->>B: Cached HTML (instant paint) B->>B: Hydrate, then interactive Note over U,B: Streaming SSR U->>S: GET /page S-->>B: Shell flushed (early paint) S-->>B: Chunk as data resolves S-->>B: Chunk as data resolves B->>B: Progressive hydrationvisualized by
The timeline diagram makes the key asymmetry visible: paint and interactivity are two separate events, and every strategy trades them off differently. SSG wins first paint. CSR loses both. Streaming SSR gets first paint close to static while still serving dynamic data. And none of them make the page interactive before the JavaScript arrives - except the last approach in this article.
Hydration: What It Actually Does
Hydration is the step where a server rendered page becomes interactive, and it is the single most misunderstood cost in modern frontend architecture.
The server sends HTML. The DOM exists, the pixels are on screen, the page looks finished. But the framework on the client has no idea what any of it is - it has no component tree, no state, no idea which node corresponds to which component, and no event listeners attached. Clicking a button does nothing.
To fix that, the framework re-executes the entire component tree on the client, rebuilding its internal representation in memory, reconciling that against the DOM the server already sent, and attaching event listeners to the existing nodes.
Read that again: it runs your whole application a second time to produce a UI that is already on screen.
That is what "hydration cost" means in practice, and it has three components:
- Download - the JavaScript for every hydrating component must arrive first.
- Parse and compile - a cost that is brutal on mid-range mobile CPUs, where it is often larger than the download cost on a decent connection.
- Execution - running the tree, which blocks the main thread. While it runs, the page cannot respond to input.
This is why a server rendered page can score beautifully on paint metrics and still feel broken. The user sees a button, clicks it, and nothing happens, because the main thread is busy replaying the app. That gap is the "uncanny valley" of SSR, and it is measured directly by Interaction to Next Paint.
The cost is proportional to the size of the hydrated tree. Which suggests the obvious fix: hydrate less.
Partial Hydration and Islands
Islands architecture starts from an observation about real pages: most of a page is not interactive. A blog post is text. A product page is images, copy, specs, and reviews - with a quantity selector and an add-to-cart button. A marketing site is almost entirely static.
Under islands, the page ships as static server rendered HTML, and only the genuinely interactive regions - the "islands" - are hydrated. Each island is independent: it has its own bundle, hydrates on its own schedule, and knows nothing about its neighbours.
Diagram100%flowchart TD subgraph Page["Server-rendered HTML shell (0 KB JS)"] H["Header / nav - static"] Hero["Hero copy + image - static"] subgraph I1["Island: Search box"] S["hydrates on visible"] end Body["Article body - static"] subgraph I2["Island: Add to cart"] C["hydrates on idle"] end subgraph I3["Island: Comments"] CM["hydrates on interaction"] end F["Footer - static"] end H --> Hero --> I1 --> Body --> I2 --> I3 --> F style Page fill:#0f172a,stroke:#334155 style I1 fill:#1e3a5f,stroke:#3b82f6 style I2 fill:#1e3a5f,stroke:#3b82f6 style I3 fill:#1e3a5f,stroke:#3b82f6visualized by
The reduction in shipped JavaScript is not incremental. A page that hydrated as one tree had to download the framework runtime plus every component. Under islands, the static regions ship zero JavaScript, and each island only loads what it needs, when it needs it - on idle, on visible, or on first interaction.
The trade is real, though. Islands are isolated by design, which makes shared state between them awkward - they cannot simply share a React context, because they are separate roots. Cross-island communication has to go through a store outside the framework, custom events, or the URL. If your page is genuinely one large interactive application, islands fight you rather than help you.
Resumability: Skipping Hydration Entirely
Islands reduce hydration. Resumability, the approach Qwik popularized, questions whether it should happen at all.
The insight is that hydration exists only because the client framework needs state the server already had and then threw away. So: do not throw it away. During server rendering, Qwik serializes the entire application state - component state, the listener graph, the relationships between them - directly into the HTML. Event handlers become attributes pointing at code that has not been downloaded.
On the client, nothing executes on load. A tiny script listens for events at the document level. When the user actually clicks something, it reads the attribute, downloads only that handler's code, and runs it.
There is no replay because there is nothing to reconstruct. The state was never lost, so the client does not need to recompute it. Startup work is close to constant regardless of application size, because the amount of JavaScript executed on load does not scale with the size of your app - it scales with what the user touches.
The costs are equally real. Serialized state adds weight to the HTML payload, and for a state-heavy app that payload can get large. Fine-grained lazy loading means an interaction may hit the network before it can respond, so the first click can feel slower than in a fully hydrated app. And the programming model has constraints that ripple through how you write components - closures have to be serializable, which is not how most developers write JavaScript.
Choosing: A Decision Framework
Do not pick a strategy for a page. Pick one per region, driven by two questions: who is this content for, and how often does it change?
- Fully static, same for everyone - marketing pages, docs, blog posts. SSG. If the volume is large enough that build time hurts, ISR.
- Same for everyone but changes often - news feeds, product listings, pricing pages. ISR with a revalidation window matched to how stale you can tolerate the data being.
- Personalized, SEO-irrelevant - dashboards, settings, authenticated app screens. CSR, or SSR only if first paint on slow devices is a real problem.
- Personalized and SEO-critical - e-commerce product pages, marketplace listings. SSR, ideally streaming so the static shell is not held hostage by the personalized query.
- Real-time - live scores, trading, collaborative editing. Server render a correct initial state, then hand off to a websocket on the client. Never try to server render a value that will be wrong by the time it reaches the browser.
The Same Page, Five Ways
Take one product page - title, images, description, price, stock status, reviews, personalized recommendations, add to cart.
- CSR: empty shell, then everything fetched client-side. Poor SEO, slow first paint on mobile, simplest server. Wrong choice for a product page.
- SSG: title, images, description, reviews baked at build. Price and stock are wrong the moment they change, so they must be patched client-side. Works if the catalogue is small and prices are stable.
- SSR: everything correct at request time, including price and stock. Costs a server render and data fetch on every request, and TTFB is hostage to the slowest query - usually recommendations.
- ISR: the SSG version with a 60-second revalidation window, so price drift is bounded. Good default for a large catalogue.
- Streaming SSR: shell, title, images, and description flush immediately. Price and stock stream in as the inventory service responds. Recommendations stream last and do not delay anything else. Best of both, at the cost of the most complex architecture.
Note that the last option is not "a different rendering mode." It is the recognition that one page contains content with four different freshness requirements, and a good architecture renders each according to its own needs.
Tradeoffs
| Option | Pros | Cons | When to Use |
|---|---|---|---|
| CSR | Cheapest server, simplest deploy, no SSR/client mismatch bugs | No SEO, slow first paint, full bundle before anything visible | Authenticated, personalized, SEO-irrelevant app screens |
| SSR | Correct data at request time, crawlable, fast first paint | Server cost per request, TTFB tied to slowest query, full hydration cost | Personalized content that must be indexed |
| SSG | Fastest possible response, near-zero serving cost, trivially cacheable | Content frozen until redeploy, build time scales with page count | Content that changes on a deploy cadence, not a data cadence |
| ISR | Static performance with bounded staleness, no build-time explosion | Users can see stale data within the window, cache invalidation is not instant | Large catalogues and feeds where seconds-old data is acceptable |
| Streaming SSR | Static-like TTFB with dynamic data, slow queries do not block the shell | Most complex to reason about, error handling per chunk, needs framework support | Pages mixing static shell, personalized, and slow data sources |
| Islands | Massive JS reduction, static regions ship zero JS | Cross-island state is awkward, wrong fit for one large interactive app | Content-heavy pages with a few interactive controls |
| Resumability | Near-constant startup cost regardless of app size, no replay | Serialized state inflates HTML, first interaction may hit the network, constrained programming model | Large apps where time-to-interactive on low-end devices is the binding constraint |
Where This Applies
Rendering architecture is the foundation the rest of this roadmap builds on. The hydration and bundle costs described here are measured and budgeted in Performance Engineering. Deciding where data is fetched - server, client, or streamed - is the direct subject of Networking and Data Fetching. And the reason server state and client state must be treated as different categories starts here, in the gap between what the server rendered and what the client believes: see State Management Architecture.
In the applied practice problems, this decision shows up first in Infinite Scroll and Real-Time Feed, where a server-rendered first page must hand off cleanly to client-side pagination and a live connection, and in Image Gallery with Lazy Loading, where generating the right priority attributes on the server is what protects LCP.
Advertisement
Your page uses SSR and still has a slow Interaction to Next Paint. Explain why, and what you would change.
SSR only fixes the time to first meaningful paint. INP is dominated by main thread work after HTML arrives, which is mostly hydration plus whatever the app does on interaction. If the whole page hydrates as one tree, the main thread is blocked while every component re-executes, so early clicks queue behind it. The fixes are architectural, not cosmetic - split the page so only interactive regions hydrate (islands), defer non critical component hydration below the fold, cut the amount of JavaScript that has to replay, or move to a resumable framework where there is no replay at all.
When would you deliberately choose CSR over SSR for a production page?
When the page is fully personalized, sits behind authentication, and has no SEO value - an admin dashboard, an analytics console, a logged in settings screen. SSR buys you crawlable HTML and a fast first paint, but for a page no crawler sees and where every byte of content depends on the user, server rendering adds server cost and TTFB while the user still waits on the data fetch. CSR with a good skeleton and an aggressively cached shell is often the better trade.
What exactly does hydration do, and why is it expensive?
Hydration walks the server rendered DOM, re-executes the component tree on the client to rebuild the framework's internal representation, and attaches event listeners to the existing nodes. It is expensive because it is pure duplicated work - the UI is already on screen, and the browser is spending main thread time producing something the user cannot see any benefit from. The cost scales with the size of the component tree and the amount of JavaScript that must be downloaded, parsed, and executed before any of it can start.
How does ISR differ from SSG, and what is the failure mode you have to design around?
SSG renders every page at build time, so content is only as fresh as the last deploy and build time grows with page count. ISR renders at build time or on first request, then re-renders in the background after a revalidation window, serving the stale page in the meantime. The failure mode is staleness you cannot control - between the content change and the next revalidation, users see old data, and a cache purge across a CDN is not instant. If correctness matters more than latency for that data, it should not be on an ISR page.
A page has a static marketing header, a personalized recommendation strip, and a live price ticker. How do you render it?
Split it by data volatility rather than picking one strategy for the whole page. The shell and the header are static and can be prerendered and cached at the edge. The recommendation strip is per user and not SEO relevant, so it streams in from the server after the shell, or hydrates as its own island. The price ticker is real time, so it renders a server provided initial value for a correct first paint and then takes over on the client via a websocket. The point of the answer is that rendering strategy is a per region decision, not a per page one.