CSR vs SSR vs SSG: Rendering Strategies
Short answer
The three differ in where and when HTML is produced. CSR builds it in the browser after JavaScript loads. SSR builds it on the server on every request. SSG builds it once at deploy time and serves the same file to everyone. Everything else — first paint, SEO, cost, freshness — follows from that choice.
| CSR | SSR | SSG | |
|---|---|---|---|
| HTML built | In the browser | On the server, per request | At build time, once |
| Initial HTML | An empty shell | Complete | Complete |
| Time to first byte | Fast | Slower — waits for data | Fastest — from a CDN |
| First contentful paint | After JS loads and runs | Immediate | Immediate |
| SEO | Depends on the crawler | Strong | Strong |
| Data freshness | Live on every view | Live on every request | As of the last build |
| Server cost per view | None | Real — compute per request | None — a static file |
| Best for | Dashboards behind a login | Personalised, SEO-critical pages | Docs, blogs, marketing, catalogues |
| Fails when | Content must be indexed | Traffic spikes | Content changes constantly |
Where the HTML comes from
Strip away the acronyms and there is one question: at the moment the browser asks for a URL, does a complete HTML document already exist? With SSG it was written to disk at build time. With SSR it is generated right then, for that request. With CSR it does not exist at all — the server sends a near-empty page plus a script bundle, and React builds the document in the browser.
1<!-- CSR: view-source on a Create React App page -->
2<div id="root"></div>
3<script src="/static/js/bundle.js"></script>
4<!-- → nothing to read, nothing to index, nothing to paint
5 until the bundle downloads, parses and executes -->
6
7<!-- SSR / SSG: the same page, rendered on the server -->
8<div id="root">
9 <h1>React Interview Kit</h1>
10 <p>40 questions with model answers…</p>
11</div>
12<script src="/static/js/bundle.js"></script>
13<!-- → readable, paintable and indexable before any JS runs.
14 The bundle then hydrates it to make it interactive. -->Note that the script tag appears in both. This is the point people miss: SSR and SSG do not remove the JavaScript. They change what the user sees while it is loading. The bundle still downloads, still runs, and still has to attach event handlers to the markup that came from the server.
What the user actually experiences
The strategies trade against each other on a timeline, and putting real metric names on that timeline is what turns a definition into an argument. Time to first byte measures how long the server took. First contentful paint is when anything appears. Largest contentful paint is when the main content appears — a Core Web Vital. Time to interactive is when clicks start working.
| Metric | CSR | SSR | SSG |
|---|---|---|---|
| TTFB | Fast — the shell is trivial | Slow — data fetch happens first | Fastest — CDN edge |
| FCP | Late — needs the JS bundle | Early | Earliest |
| LCP | Late — often after a second fetch | Early | Earliest |
| TTI | After hydration | After hydration | After hydration |
| Subsequent navigations | Fastest — client-side routing | Fast, with a server round trip | Fast, prefetched |
| Behaviour with JS disabled | Blank page | Content renders | Content renders |
Two things stand out. First, TTI is the same in all three, because interactivity always waits for hydration — server rendering buys you a faster paint, not a faster click. Second, CSR is not universally slower: once loaded, its client-side navigations avoid the server entirely, which is why app-like products with long sessions still choose it.
Client-side rendering
CSR is the default of a plain React or Vite app. The server's job is to hand over static assets; everything else happens in the browser. That is a genuinely good fit for software people log into and use for a long time — an admin panel, an editor, an internal tool — where the first load happens once and the content is private anyway.
1// index.html → bundle.js → React mounts → useEffect fires → fetch → render
2function Dashboard() {
3 const [data, setData] = useState(null);
4
5 useEffect(() => { fetch("/api/stats").then(r => r.json()).then(setData); }, []);
6
7 if (!data) return <Spinner />;
8 return <Charts data={data} />;
9}
10// → the request for /api/stats cannot start until the bundle has
11// downloaded, parsed and run. On 4G that is often 2-3 seconds
12// of spinner before the network call even begins.Server-side rendering
SSR runs your components on the server for each request, producing complete HTML that the browser can paint immediately. It is the right answer when a page must be indexable and its content is personalised or genuinely live — a logged-in feed, a search results page, a dashboard with an SEO-visible summary.
The cost is on the server. Every request does work, which means compute you pay for, latency you must control, and a failure mode where a traffic spike or a slow database query degrades every visitor rather than just one. It also moves your data-fetching latency into TTFB, so an unindexed query on the server makes the whole page feel slow rather than just one widget.
1// app/feed/page.tsx — a Server Component that opts out of caching
2export const dynamic = "force-dynamic";
3
4export default async function Feed() {
5 const posts = await db.post.findMany({ take: 20 });
6 return <PostList posts={posts} />;
7}
8// → runs on every request. The HTML arrives complete, and the
9// database query time is added to TTFB for every visitor.This is 1 of 200+ questions in the Complete Frontend Kit
Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the Complete Frontend Kit → ₹499Static generation and ISR
SSG renders every page once, at build time, and the result is a file. A CDN serves it from an edge location near the user, which is as fast as the web gets and costs essentially nothing per view. It is unbeatable for content that does not change per visitor: documentation, blog posts, marketing pages, landing pages, product catalogues.
The historical objection was staleness — new content required a full rebuild, which does not scale to ten thousand pages. Incremental Static Regeneration removes that. Pages are served static, and after a revalidation window the next request triggers a background rebuild of that one page while still serving the old version instantly.
1// app/kits/[slug]/page.tsx
2export async function generateStaticParams() {
3 const kits = await getKits();
4 return kits.map(k => ({ slug: k.slug }));
5}
6// → these pages are built at deploy time
7
8export const revalidate = 3600;
9
10export default async function Kit({ params }) {
11 const kit = await getKit(params.slug);
12 return <KitPage kit={kit} />;
13}
14// → served from the CDN. One hour after a page was generated, the
15// next visitor still gets the cached copy instantly while a fresh
16// one is built in the background for everyone after them.Hydration: the part that is easy to skip
Server-rendered HTML is not interactive. React has to load in the browser, walk the existing DOM, and attach its event handlers and internal state to it — that is hydration. Until it finishes, the page looks ready and does nothing, which is a worse experience in some ways than an honest spinner, because users click and nothing happens.
Hydration also demands that the server's markup and the client's first render match exactly. When they do not — because you rendered a Date, a random value, or something read from localStorage — React logs a hydration mismatch and discards the server HTML for that subtree, throwing away the benefit you paid for.
1function Greeting() {
2 return <p>Rendered at {new Date().toLocaleTimeString()}</p>;
3}
4// → Warning: Text content did not match.
5// Server: "10:42:01" Client: "10:42:04"
6
7// Fix: render the deterministic version, fill in on the client
8function Greeting() {
9 const [time, setTime] = useState(null);
10 useEffect(() => setTime(new Date().toLocaleTimeString()), []);
11 return <p>Rendered at {time ?? "…"}</p>;
12 // → server and client both produce "…" on the first pass,
13 // so hydration matches and the value appears after mount
14}React Server Components, the fourth option
RSC changes the terms of the debate rather than adding a variant. A Server Component runs only on the server, and its JavaScript is never sent to the browser at all — what ships is the rendered output. So a page can be server-rendered without paying the hydration cost for the parts that are not interactive, and the client bundle shrinks to just the interactive islands.
1// app/kits/page.tsx — a Server Component, no "use client"
2import AddToCart from "./add-to-cart";
3
4export default async function Kits() {
5 const kits = await db.kit.findMany(); // runs on the server
6 return kits.map(k => (
7 <article key={k.id}>
8 <h2>{k.title}</h2>
9 <AddToCart id={k.id} /> {/* the only client JS */}
10 </article>
11 ));
12}
13// → the markup and the database code never reach the browser;
14// only add-to-cart.js is hydratedStreaming completes the picture. With Suspense boundaries, the server sends the shell immediately and streams each slower section in as its data resolves, so a single slow query no longer holds up the entire page's TTFB. Together these mean the practical modern answer is rarely one strategy for a whole app — it is server rendering with static caching where possible, and client interactivity scoped to the components that need it.
Choosing, per route
The useful framing is that these are per-route decisions, not per-application ones. Next.js, Remix, Nuxt and SvelteKit all let a single project mix them, and a well-built product does. Work through it by asking who the content is for and how often it changes.
- Is the content public and does it need to be found in search? If not, CSR is on the table.
- Is it the same for every visitor? If yes, generate it statically.
- How often does it change? Rarely means plain SSG; regularly means ISR with revalidation on write.
- Is it personalised or genuinely live? That is SSR, or a server component rendered per request.
- Is the interactive part small? Keep it as a client island rather than making the page client-rendered.
- Would a slow data source block the whole page? Wrap it in Suspense and stream it.
| Page | Strategy | Why |
|---|---|---|
| Marketing home page | SSG | Identical for everyone, changes rarely |
| Blog or docs article | SSG | Same, and there may be thousands of them |
| Product listing | ISR | Public and indexable, but stock and price move |
| Search results | SSR | Unique per query, still worth indexing |
| Logged-in dashboard | CSR or RSC | Private, so SEO is irrelevant |
| Checkout | CSR | Interaction-heavy, never indexed |
| User profile page (public) | ISR or SSR | Indexable but user-specific and editable |
What is hydration in React?
The process of attaching React to server-rendered HTML in the browser — walking the existing DOM, building the component tree, and wiring up event handlers and state without recreating the markup. Until it completes, the page is visible but not interactive.
What is ISR?
Incremental Static Regeneration: pages are static, but after a revalidation window the next request serves the cached copy instantly and triggers a background rebuild of that single page. It gives static delivery with periodic freshness and no full redeploy.
Is SSR always better for SEO than CSR?
It is more reliable. Googlebot does render JavaScript, but on a deferred pass, and many other crawlers and link-preview bots do not render at all. Server-rendered HTML removes the dependency entirely, which matters most for pages you need indexed quickly or shared on social platforms.
What is the difference between SSG and pre-rendering?
Pre-rendering is the umbrella term for producing HTML before the browser asks for it, which covers both SSG and SSR. SSG is the build-time variant specifically. Next.js documentation used pre-rendering as the general word for both, which is where the confusion comes from.
Do Server Components replace SSR?
No — they are a component model, not a rendering strategy, and they work alongside all of them. What they change is how much JavaScript reaches the browser: a Server Component's code never ships, so a server-rendered page can skip hydration for everything that is not interactive.
Frequently asked questions
- What is the difference between CSR, SSR and SSG?
- CSR builds the HTML in the browser after the JavaScript bundle loads. SSR builds it on the server for each request. SSG builds it once at deploy time and serves the same file to everyone. That one difference determines first paint, SEO reliability, data freshness and server cost.
- Which is best for SEO?
- SSG and SSR, because the content is in the HTML before any JavaScript runs, so every crawler and link-preview bot sees it. CSR relies on the crawler executing JavaScript, which Googlebot does on a deferred pass and many others do not do at all.
- What is hydration?
- Attaching React to HTML that was rendered on the server — building the component tree in the browser and wiring up event handlers to the existing DOM instead of recreating it. Before hydration completes the page looks finished but does not respond to clicks.
- What is ISR in Next.js?
- Incremental Static Regeneration. Pages are generated statically but carry a revalidation window; after it expires, the next request is still served the cached copy while a fresh version is built in the background. You can also trigger it on demand with revalidatePath or revalidateTag.
- Is SSR slower than CSR?
- SSR has a slower time to first byte, because the server fetches data before responding, but a much faster first contentful paint, because complete HTML arrives. CSR reverses that. For a first visit SSR almost always feels faster; for subsequent in-app navigation CSR usually wins.
- When should I use client-side rendering?
- For private, interaction-heavy software where SEO is irrelevant and sessions are long — admin panels, editors, dashboards behind a login, checkout flows. The one-time load cost is amortised, and client-side routing makes every navigation after that instant.
- What causes a hydration mismatch error?
- The server's HTML and the client's first render producing different output — typically from dates, random values, browser-only APIs like localStorage or window, or locale-dependent formatting. React discards the server markup for that subtree, which wastes the server rendering you paid for.
- Can one app use all three strategies?
- Yes, and good ones do. Next.js, Remix, Nuxt and SvelteKit all decide per route, so marketing pages can be static, search results server-rendered, and the dashboard client-rendered — all in one deployment.
- What is the difference between SSG and ISR?
- SSG builds every page at deploy time and they stay fixed until the next build. ISR builds them the same way but allows individual pages to be regenerated afterwards, either on a timer or on demand, without redeploying the whole site.
- Do React Server Components replace SSR?
- No. They are a component model that runs on the server and never ships its JavaScript to the browser, and they compose with server rendering rather than replacing it. Their benefit is a smaller client bundle and less hydration, not a different place to generate HTML.
- What is streaming SSR?
- Sending the HTML in chunks as it becomes ready instead of waiting for the whole page. With Suspense boundaries the shell arrives immediately and slower sections stream in as their data resolves, so one slow query no longer delays the entire response.
- Which rendering strategy should I use for a blog?
- SSG, with ISR if posts are edited after publishing. The content is identical for every visitor and changes rarely, so building once and serving from a CDN gives the best possible first paint at essentially zero per-view cost.
This is 1 of 200+ questions in the Complete Frontend Kit
Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the Complete Frontend Kit → ₹499