Topic 20.1
CSR, SSR, SSG, ISR and Streaming
In one line
Rendering strategies differ in where and when a page's HTML is made: in the browser (CSR), on the server for each request (SSR), once at build time (SSG), at build time and then refreshed (ISR), or on the server in pieces as data arrives (streaming); hydration then makes server HTML interactive.
Think of it like this
Getting a birthday cake. CSR is a kit in a box: flour, eggs and a recipe arrive, and you bake it at home before you can eat anything. SSR is a bakery that bakes your cake fresh when you order it. SSG is a shop shelf of cakes baked this morning: instant, but the same for everyone. ISR is that shelf, re-baked every hour. Streaming is a restaurant that brings the starter first and the main course when it's ready, instead of making you wait for everything.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Rendering strategy
- The plan for where (browser or server) and when (build, request, later) a page's HTML is made.
- CSR
- Client-side rendering: the browser gets an empty page plus JavaScript and builds the HTML itself.
- SSR
- Server-side rendering: the server runs your components for each request and sends finished HTML.
- SSG
- Static site generation: pages are rendered once at build time and saved as files.
- ISR
- Incremental static regeneration: static pages that are rebuilt in the background after a time limit or on demand.
- Hydration
- React taking over server-made HTML in the browser: attaching event handlers and state without rebuilding the DOM.
- Streaming
- Sending the HTML in pieces: a shell first, then each slow part when its data is ready.
- Shell
- The part of the page outside any Suspense boundary, sent first in streaming SSR.
- TTFB
- Time to first byte: how long until the first bit of the server's answer reaches the browser.
- CDN
- A content delivery network: many servers around the world that keep copies of files close to users.
Step by step
01CSR: the browser does all the work
The server sends a tiny HTML file and a script tag. Nothing useful is on screen until the browser has downloaded, parsed and run the bundle, and often fetched data too. On a slow phone this can take seconds.
02SSR: HTML first, interactivity after hydration
The server runs the components, so the first response already contains the products. The browser paints it immediately. The script still loads, and hydrateRoot attaches React to the existing HTML. Until then, clicks do nothing.
03SSG and ISR: render ahead of time
With SSG the rendering happens during npm run build. Every request gets the same saved file, so the server does no work at all. ISR adds a timer: after revalidate seconds the next request still gets the saved page, and a fresh one is built in the background for the visitors after it.
04Streaming: shell now, slow parts later
Wrap slow parts in <Suspense>. The server sends the shell and the fallbacks right away, then streams each boundary's real HTML when its data is ready. One slow query no longer blocks the whole page.
import { renderToReadableStream } from "react-dom/server";
export async function handleRequest() {
// resolves as soon as the SHELL is ready, not the whole page
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ["/client.js"], // the script that will call hydrateRoot
});
return new Response(stream, { headers: { "Content-Type": "text/html" } });
}05Pick per page
A shop might use SSG for the About page, ISR for product pages (refreshed every few minutes), streaming SSR for the personalised home page, and plain client rendering inside the logged-in dashboard. Frameworks let you choose per route, and Next.js 16 lets one page be part static, part streamed.
Try it yourself
- 1
Click before the JavaScript arrives
In "Hydration: visible first, interactive later", change the 300 ms delay to 2000 and click the button yourself as soon as the page appears: during those two seconds the page looks finished. Predict what a real user's clicks do in that window. (Nothing: the HTML has no event handlers yet. This gap is why big bundles hurt SSR pages.)
- 2
Two boundaries, two chunks
In the streaming example, add a second component
Specialsthat waits 300 ms, inside its own<Suspense fallback={<p>Loading specials...</p>}>. Predict how many chunks arrive and in what order. Then put both components inside one boundary and compare: now the faster one waits for the slower one. - 3
Shorter revalidate
In the ISR model, set
revalidateto 5. Predict which request now serves stale data, then run it.
Code & diagrams
`renderToString` is the real server API from `react-dom/server`, running here in your browser to play the server. Notice the apostrophe became `'`: React escapes text so it is safe HTML.
Preview
Console
Nothing logged yet.Expected console output
CSR first HTML: <div id="root"></div>
SSR first HTML: <div id="root"><main><h1>Asha's Shop</h1><ul><li>Pen</li><li>Notebook</li><li>Bag</li></ul></main></div>Simulated server: `renderToString` makes the HTML and we put it in the page by hand, as a server response would. `hydrateRoot` is real: it reuses the very same `<button>` element and only attaches the click handler.
Preview
Console
Nothing logged yet.Expected console output
Before hydration: Clicked 0 times
After hydration: Clicked 1 times
Same <button> element: true`renderToReadableStream` is real. The `receive` function plays the browser's part: it appends each chunk and runs React's small inline script, which moves the finished menu into the place of the fallback. No React code runs in the "browser" for that swap.
Preview
Console
Nothing logged yet.Expected console output
after chunk 1: Cafe | Loading menu...
after chunk 2: Cafe | Tea | CoffeeA model in plain JavaScript with a fake clock (the numbers are seconds). `renderToStaticMarkup` makes HTML without React's hydration markers, which is what a pure static page needs.
Preview
Console
Nothing logged yet.Expected console output
SSG request 1 <p>Pens in stock: 5</p>
SSG request 2 <p>Pens in stock: 5</p>
SSG request 3 <p>Pens in stock: 5</p>
SSG after stock changed: <p>Pens in stock: 5</p> | renders: 1
SSR request 1 <p>Pens in stock: 4</p>
SSR request 2 <p>Pens in stock: 4</p>
SSR renders: 2
ISR at 10s: <p>Pens in stock: 4</p>
ISR at 70s: <p>Pens in stock: 4</p>
ISR at 75s: <p>Pens in stock: 3</p>
ISR renders: 2Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use createRoot on server HTML
In the hydration example, replace hydrateRoot(root, <Counter />) with createRoot(root).render(<Counter />) (import createRoot).
Myth vs fact
Myth
SSR makes a page interactive sooner.
Fact
SSR makes content visible sooner. Interactivity still waits for the JavaScript to download and hydrate, sometimes later than CSR because the HTML is bigger. Less client JavaScript (Server Components, Topic 20.2) is what makes interactivity come sooner.
Myth
Static pages can't show changing data.
Fact
ISR rebuilds them on a timer or on demand, and parts of a static page can still load fresh data on the client or stream it from the server.
Myth
Search engines can't read client-rendered apps at all.
Fact
Google runs JavaScript, but later and with limits; many other crawlers and link-preview bots don't. Server HTML is the reliable way to be indexed and previewed.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
renderToStringdoes not wait for Suspense: suspended parts are sent as their fallback and the client must render them. UserenderToReadableStream(Web Streams: Deno, Bun, edge runtimes, and Node 18+) orrenderToPipeableStream(Node streams), which support streaming and waiting. For fully static output,prerenderfromreact-dom/staticwaits for all data. - ▸
The streamed HTML uses markers:
<!--$?-->with a<template id="B:0">for a pending boundary, a hidden<div id="S:0">for the finished content, and a small$RCscript that swaps them. React 19.2 batches these reveals for a short time so several boundaries that finish close together appear together instead of popping in one by one. - ▸
onShellReady(pipeable) or the resolved promise (readable) is your chance to set the status code before streaming. Once bytes have been sent you can't change the HTTP status, so an error inside a boundary later can only be shown in the page, not as a 500. - ▸
Bots and crawlers often get a non-streamed, fully rendered response: Next.js detects them and waits for the whole page, and React's
allReady/prerenderserve the same purpose in custom servers.
Remember this
- 1
Rendering here means turning components into HTML. Client-side rendering (CSR) makes the HTML in the browser: the server sends an almost empty page (
<div id="root"></div>) and a JavaScript bundle; the browser downloads and runs it, and only then can the user see anything. Every Vite app from earlier phases is CSR. It is simple to host (static files) and great after the first load, but the first visit shows a blank page while JavaScript downloads, and a search engine or link preview that doesn't run JavaScript sees nothing. - 2
Server-side rendering (SSR) runs your components on the server for each request with
react-dom/server(renderToString, or the streamingrenderToReadableStream/renderToPipeableStream) and sends finished HTML. The user sees content as soon as the HTML arrives. But that HTML is not interactive yet: buttons do nothing until the JavaScript loads and React hydrates the page. Hydration means React runs the same components in the browser, walks the existing HTML, attaches event handlers and state to it, and takes over without rebuilding it. You start it withhydrateRoot(container, <App />)instead ofcreateRoot(Topic 20.6). - 3
Static site generation (SSG) renders pages once, at build time, and saves them as
.htmlfiles. A CDN (a network of servers near users) can serve them instantly, and the server does no work per request. The cost: the content is frozen until the next build, so it suits docs, blogs and marketing pages. Incremental static regeneration (ISR) keeps SSG's speed but rebuilds a page in the background after it is older than a time you choose (say 60 seconds) or when you tell it to; visitors in between get the saved page, and the first visitor after the time limit still gets the old page while the new one is built (stale-while-revalidate). This site, Hectal, is statically generated at build time. - 4
Each strategy trades speed of first content against freshness and server cost. Three timings explain it. TTFB (time to first byte) is how long until the first byte of the response arrives: tiny for static files, longer for SSR because the server renders first. FCP/LCP (first and largest contentful paint) is when the user first sees content and the main content: late for CSR, early for SSR and SSG. Interactivity is when clicks actually work: for SSR and SSG that is after hydration, so a page can look ready but ignore clicks for a moment (the "uncanny valley"). Responsiveness after that is measured by INP (interaction to next paint).
- 5
Streaming SSR fixes SSR's biggest problem: a slow piece of data blocking the whole page. With
renderToReadableStream, React sends the shell (everything outside<Suspense>boundaries, with the boundaries' fallbacks) immediately, keeps the connection open, and when a suspended part's data is ready it sends that HTML plus a tiny inline script that swaps it into place, even before any React JavaScript has loaded. The browser shows the header and a "Loading reviews..." line at once, then the reviews pop in. Suspense (Topic 14.3) is therefore not only a client feature: it marks the pieces the server may send later. - 6
React 18 also added selective hydration: with streaming, each Suspense boundary hydrates on its own as soon as its HTML and code are ready, and if the user clicks inside a part that isn't hydrated yet, React hydrates that part first. Modern frameworks mix strategies per page or even per component: Next.js prerenders a static shell at build time and streams the dynamic holes at request time (partial prerendering, Topic 20.5).
Explain it without notes
Compare CSR, SSR and SSG: where and when is the HTML made, and what are the costs of each?
What is hydration and why is a server-rendered page not interactive straight away?
How does streaming SSR with Suspense improve on plain SSR?
What is ISR and what does stale-while-revalidate mean?
Practice
Use renderToString to log the HTML for <Badge label="New" />, where Badge returns <span className="badge">{label}</span>.
Choose a strategy for each page of a news site: About page, an article, the live-scores widget, the logged-in reader's saved list. Give one reason each.
Trade-offs
- ↔
SSR and streaming need a running server (or serverless functions) and cost compute per request; SSG and CSR can be served from any static host for almost nothing.
- ↔
Server HTML speeds up first paint but adds hydration work and the risk of hydration mismatches (Topic 20.6); a page that is mostly interactive (an editor, a game) gains little from it.
- ↔
Static generation is the fastest and cheapest delivery, but every content change needs a rebuild or a revalidation strategy.
Done when you can
I can explain CSR, SSR, SSG, ISR and streaming in one sentence each.
I can explain hydration and why server HTML isn't interactive at first.
I can use
renderToString,renderToReadableStreamandhydrateRoot.I can relate TTFB, first paint and interactivity to each strategy.
I can pick a rendering strategy per page and justify it.