#CSR, SSR, SSG and ISR
Every React app has to turn components into HTML at some point. The rendering strategies differ in where that happens (browser or server) and when (per request, at build time, or on a schedule):
- CSR — client-side rendering. The server sends an almost empty HTML page and a JavaScript bundle; the browser downloads the JS, runs React, fetches data and builds the page. Classic Vite or Create React App SPAs.
- SSR — server-side rendering. On every request the server runs React, produces the full HTML for that URL and sends it. The browser shows it immediately, then downloads the same JS and hydrates it to make it interactive.
- SSG — static site generation. The same render as SSR, but done once at build time for every known page; the HTML files are served from a CDN like any static file.
- ISR — incremental static regeneration. SSG with an expiry: pages are generated at build time (or on first request), served from the cache, and re-generated in the background after a set time or when you invalidate them. A Next.js term; other frameworks call it stale-while-revalidate caching of rendered pages.
The mental model is a restaurant. CSR hands you the ingredients and a recipe (the JS bundle) and you cook at your table. SSR cooks each dish to order in the kitchen. SSG cooks everything before opening and keeps it warm. ISR is SSG where the kitchen quietly replaces a dish when it's been out too long. The diner (user) sees food soonest with SSG, then SSR; with CSR they wait for their own cooking, but once the kitchen is at the table every later dish (client-side navigation) is fast.
import { useState } from "react";
export function App({ products }) {
const [cart, setCart] = useState(0);
return (
<main>
<h1>Shop</h1>
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name}: ${p.price}
</li>
))}
</ul>
<button onClick={() => setCart((c) => c + 1)}>Cart: {cart}</button>
</main>
);
}
// CSR: what the server sends for every URL
// <!doctype html><html><head><title>Shop</title></head>
// <body><div id="root"></div><script type="module" src="/assets/index-a1b2c3.js"></script></body></html>
// SSR: renderToString(<App products={products} />) on the server gives
// <main><h1>Shop</h1><ul><li>Keyboard<!-- -->: $<!-- -->80</li><li>Mouse<!-- -->: $<!-- -->25</li></ul>
// <button>Cart: <!-- -->0</button></main>- With CSR the HTML body contains no text at all — the test strips the tags from the CSR shell and gets an empty string. A user (or a crawler that doesn't run JavaScript) sees a blank page until
index-a1b2c3.jsdownloads, React runs, and the products are fetched and rendered. - With SSR the server calls
renderToString(<App products={products} />)(run in a Node test) and gets the full markup: the heading, both products and the button. That HTML is the first thing the browser receives, so text is visible before any JavaScript runs. - The
<!-- -->comments are React marking where one text node ends and the next begins:Keyboard,: $and80are three separate text children in the JSX. On the client, hydration uses these markers to line up its text nodes with the server's. - The server-rendered button is visible but not yet interactive:
onClickhandlers don't exist in HTML. Clicking it before hydration does nothing (a test in Hydration proves it). The page becomes interactive once the client bundle loads and React hydrates. - SSG produces exactly the same HTML as SSR — it just runs at build time and stores the result. The difference between the strategies is when and how often this render happens, not what it produces.
From my notes, when to use each, with my corrections inline:
- Use CSR when you're building a highly interactive SPA where the experience is mostly client-side after the first load; when SEO isn't a concern, such as dashboards and apps used by logged-in users; and when initial load time matters less than interactivity afterwards. All still true.
- Use SSR when SEO is crucial — blogs, e-commerce, any content-heavy site you want indexed. True, but SSG is just as good for SEO and cheaper when the content doesn't depend on the request (see SEO for React apps).
- Use SSR for a faster initial load on slow networks, because users don't wait for JavaScript to see content. Mostly true, with a caveat my notes don't mention: content is visible sooner, but the page isn't interactive until the same JavaScript has downloaded and hydrated, and every request now waits for the server to render (a slower time to first byte). A page that looks ready but ignores clicks is its own kind of bad experience.
- "Static content: SSR (with caching) can be a good fit." Correcting my notes: content that rarely changes is the case for SSG or ISR, not per-request SSR. Rendering the same HTML on every request just to cache it is what SSG does once at build time.
| CSR | SSR | SSG | ISR | |
|---|---|---|---|---|
| HTML rendered | In the browser | On each request | At build time | At build time, re-generated in the background |
| First content visible | Late (after JS + data) | Early | Earliest (CDN) | Earliest (CDN) |
| Interactive | After JS | After JS + hydration | After JS + hydration | After JS + hydration |
| Data freshness | Live | Live per request | As of the last build | As of the last regeneration |
| Server cost | Static hosting | A server renders every request | Static hosting | Static hosting + a server to regenerate |
| Typical use | Dashboards, internal tools | Personalised or fast-changing pages | Docs, blogs, marketing | Product pages, news, large catalogues |
Modern frameworks don't make you pick one for the whole app. Next.js, React Router's framework mode and TanStack Start choose per route (static for the marketing pages, server-rendered for the account page) and even per component (a static shell with dynamic parts streamed in — see Streaming SSR and Next.js App Router in brief).