CSR, SSR, SSG & ISR
Every React app has to answer one question before the user sees a pixel: where and when does the HTML get built? The answer — in the browser, on the server per request, at build time, or some hybrid — is your rendering strategy. It shapes how fast the first paint arrives, whether search engines can read your content, how much it costs to serve traffic, and how quickly the page becomes interactive. The four strategies below (CSR, SSR, SSG, ISR) are points on a spectrum, and modern frameworks let you mix them route by route.
Client-side rendering (CSR)
CSR is the classic single-page-app model. The server sends a near-empty HTML shell plus a JavaScript bundle. The browser downloads and parses that bundle, runs React, fetches data, and only then renders the UI into the empty <div id="root">.
// main.jsx — a plain Vite SPA
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")).render(<App />);
The HTML the server actually ships looks like this:
Output:
<!doctype html>
<html>
<body>
<div id="root"></div>
<script type="module" src="/assets/index-a1b2c3.js"></script>
</body>
</html>
Nothing is visible until JavaScript runs. That hurts time-to-first-contentful-paint and means crawlers that don’t execute JS see an empty page. The upside: after the initial load, navigation is instant because routing happens entirely in the browser, and hosting is cheap (static files on a CDN). CSR is the right call for dashboards, internal tools, and apps behind a login where SEO is irrelevant.
Server-side rendering (SSR)
With SSR, React runs on the server for every request, produces fully formed HTML, and sends it down. The browser paints immediately, then downloads the bundle and hydrates — attaching event listeners to make the static markup interactive.
// server.js — Express + React 18 streaming SSR
import express from "express";
import { renderToPipeableStream } from "react-dom/server";
import App from "./App.jsx";
const app = express();
app.get("*", (req, res) => {
const { pipe } = renderToPipeableStream(<App url={req.url} />, {
bootstrapScripts: ["/assets/client.js"],
onShellReady() {
res.setHeader("content-type", "text/html");
pipe(res);
},
});
});
app.listen(3000);
SSR gives you fresh, per-request content and full SEO, at the cost of running a server on every hit — higher TTFB than static delivery and real compute cost under load. Use it for personalized pages, authenticated content, or anything that must reflect data that changes by the second.
Static site generation (SSG)
SSG renders every page to HTML once at build time. The output is a folder of plain .html files you can drop on any CDN. There’s no server doing per-request work, so TTFB is as low as physics allows and the SEO is perfect.
// Next.js (Pages Router) — runs at build time
export async function getStaticProps() {
const posts = await fetch("https://api.example.com/posts").then((r) => r.json());
return { props: { posts } };
}
export default function Blog({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
The trade-off is freshness: data is frozen at build time. If your catalog of 50,000 products changes hourly, rebuilding the whole site for every update is impractical. SSG shines for blogs, docs, marketing pages, and anything that changes on a human (not machine) cadence.
Incremental static regeneration (ISR)
ISR is SSG with an expiry date. Pages are served statically from the CDN, but after a configured interval the framework rebuilds them in the background on the next request, then swaps in the fresh version. You get static-fast delivery and reasonably current data without a full rebuild.
export async function getStaticProps() {
const product = await fetch("https://api.example.com/product/42").then((r) => r.json());
return {
props: { product },
revalidate: 60, // regenerate at most once per 60 seconds
};
}
The first visitor after the window sees the stale page (served instantly), while a fresh copy is generated for everyone after. This “stale-while-revalidate” behavior makes ISR ideal for large e-commerce catalogs, news sites, and content that updates often but tolerates a few seconds of lag.
Tip: “Stale on first hit, fresh afterwards” is the mental model for ISR. If a page must never show outdated data to anyone, use SSR instead.
Comparing the strategies
| CSR | SSR | SSG | ISR | |
|---|---|---|---|---|
| HTML built | In browser | Per request (server) | At build time | At build, refreshed on interval |
| TTFB | Fast (empty shell) | Slower (compute) | Fastest (CDN) | Fastest (CDN) |
| Data freshness | Real-time (client fetch) | Real-time | Frozen at build | Eventually fresh |
| SEO | Poor without prerender | Excellent | Excellent | Excellent |
| Server cost | None | Highest | None | Low |
| Best for | Dashboards, apps behind auth | Personalized / live data | Blogs, docs, marketing | Large catalogs, news |
A useful way to picture the request flow:
Output:
CSR: request → empty HTML → download JS → render → fetch → paint
SSR: request → run React on server → full HTML → paint → hydrate
SSG: request → serve prebuilt HTML from CDN → paint → hydrate
ISR: request → serve cached HTML → paint → (background rebuild if stale)
Choosing the right one
Don’t pick a single strategy for the whole app — pick per route. Frameworks like Next.js and Remix let a marketing homepage be static, a product page use ISR, a personalized feed use SSR, and an admin dashboard fall back to CSR, all in one codebase. Start by asking two questions for each route: Does this content change per user or per second? (→ SSR) and Does SEO matter? (→ avoid pure CSR). Everything else is an optimization toward static delivery.
Best Practices
- Decide rendering per route, not per app — mix static, ISR, and SSR where each fits.
- Default to static (SSG) and only escalate to ISR or SSR when freshness or personalization demands it.
- Reserve pure CSR for authenticated, SEO-irrelevant surfaces like dashboards and internal tools.
- Use ISR with a sensible
revalidatewindow to balance freshness against rebuild and origin cost. - Keep hydration cheap: ship less client JS so SSR/SSG pages become interactive sooner.
- Measure real metrics (TTFB, LCP, INP) before and after — strategy choices should be driven by data, not vibes.