CSR, SSR, SSG, ISR — difference between rendering strategies
CSR, SSR, SSG, and ISR - four rendering strategies that decide when and where your app builds HTML: in the browser after JS loads, on the server per request, at build time as static files, or as a cached page that rebuilds on a timer.
Theory
TL;DR
- CSR = server sends
<div id="root"></div>, browser downloads JS, fetches data, builds the page. Nothing visible until JS runs. - SSR = server generates full HTML on every request. Browser displays it immediately, JS hydrates after.
- SSG = HTML built once at
npm run build, served as static files from a CDN. No server involved at runtime. - ISR = SSG with a revalidation timer or on-demand trigger. Page rebuilds in the background when stale, cached version keeps serving.
- Decision rule: static content use SSG, personalized pages use SSR, product pages use ISR, admin panels use CSR.
Quick example
// Next.js 14 App Router - all four in one file
// SSG (default): built once at deploy, served from CDN
export default async function StaticPage() {
const res = await fetch('https://api.example.com/posts'); // cached at build
const data = await res.json();
return <div>{data.title}</div>;
}
// ISR: cached, rebuilds every 60 seconds
export const revalidate = 60;
// SSR: fresh HTML on every request
const res = await fetch('https://api.example.com/user', { cache: 'no-store' });
// CSR: server sends empty shell, useEffect fills it in
'use client';
useEffect(() => { fetch('/api/data').then(setData); }, []);SSG and ISR deliver full HTML from a CDN. SSR delivers full HTML from a server on each request. CSR delivers an empty shell and lets the browser do the work.
Key difference
The split comes down to timing and location. CSR does all rendering in the browser after JS downloads, so the first thing a user sees is a blank screen. SSR and SSG both send complete HTML, which is why they score better on SEO and First Contentful Paint. The real gap between SSG and ISR is freshness: SSG pages never change unless you redeploy, while ISR pages can update in the background without a full deploy, using a timer or a CMS webhook.
When to use
- Blog posts, docs, marketing pages: SSG. Content changes rarely, CDN delivery adds no server cost.
- User dashboard, social feed, authenticated pages: SSR. Data is user-specific, caching at the CDN level doesn't apply.
- E-commerce product pages (price, stock): ISR with
revalidate: 60or on-demand viarevalidateTag('product'). Stale-while-revalidate keeps response times fast. - Admin panels, internal tools, SPAs: CSR. SEO is irrelevant, and you get full client-side interactivity without hydration overhead.
- Mixed app (docs plus search plus profile): hybrid. SSG for static routes, SSR for personalized ones, CSR for interactive widgets like autocomplete or charts.
Comparison table
| Aspect | CSR | SSR | SSG | ISR |
|---|---|---|---|---|
| Render location | Browser (JS bundle) | Server per request | Build time | Build + background revalidation |
| Initial HTML | Empty <div id="root"> | Full, semantic | Full, semantic | Full, cached (refreshes later) |
| TTFB | Fast (tiny HTML file) | Slower (server compute) | Fastest (CDN hit) | Fast (cached, revalidates in background) |
| SEO | Poor | Excellent | Excellent | Excellent |
| Server load | None after deploy | High per request | None at runtime | Low (incremental rebuilds) |
| Dynamic data | Client API calls | Server fetch per request | Requires full rebuild | Timer or API trigger |
| Example tools | Vite + React, Create React App | Next.js, Remix | Gatsby, Next.js static export | Next.js revalidate |
| Best for | SPAs, admin panels | Personalized pages | Blogs, docs | News, product pages |
How it works internally
CSR: browser receives a minimal HTML shell, downloads the JS bundle, Chrome's V8 parses and executes it, React's reconciler builds the virtual DOM, then writes to the real DOM. Nothing visible until that full cycle completes. On slow connections, this means several seconds of blank screen.
SSR: Node.js runs React's renderToString() (or renderToPipeableStream() in React 18 for streaming) on the server, sends full HTML in the response. Browser displays it immediately. Then the JS bundle loads and React hydrates: it attaches event listeners to the existing HTML without re-rendering. If client and server produce different markup, you get a hydration mismatch warning and React re-renders from scratch on the client.
SSG: at next build, Next.js prerenders every static page and writes HTML files to disk. Those files go to a CDN. Every user hits the CDN edge node closest to them, no server involved.
ISR: same as SSG, but each page has a TTL. After it expires, the next incoming request triggers a background rebuild. The current cached page keeps serving while the new one builds, then replaces the cache atomically. Next.js also supports on-demand revalidation via revalidatePath() or revalidateTag(), which is useful when a CMS publishes new content and you want the page updated within seconds rather than waiting for the timer.
Common mistakes
Using SSR for static content. A developer adds SSR to a blog. Every page load hits the server. The Vercel bill triples. Fix: export const dynamic = 'force-static' in Next.js, or switch to SSG. Nothing on a blog needs per-request rendering.
Forgetting ISR revalidation on product pages. Default behavior in Next.js is SSG with no revalidation, so prices go stale and nobody notices until a customer pays an old price. Fix:
// app/products/[id]/page.tsx
export const revalidate = 300; // rebuild every 5 minutes
// app/api/revalidate/route.ts - triggered by CMS webhook
import { revalidateTag } from 'next/cache';
export async function POST(request: Request) {
const { productId } = await request.json();
revalidateTag(`product-${productId}`); // rebuilds only this product's page
return Response.json({ revalidated: true });
}CSR without code splitting. A 2MB JS bundle means 10+ seconds before the page is interactive. React parses and executes everything upfront. Fix: React.lazy with Suspense:
const ProductList = lazy(() => import('./ProductList'));
// Loads only when the component is needed, not on initial page loadHydration mismatch in SSR. Client renders something different from what the server sent, usually because of timestamps, random IDs, or browser-only APIs like window. React 18 logs a warning and re-renders the component tree from scratch. Fix: move client-only logic into useEffect, or use suppressHydrationWarning on elements with known differences like formatted dates.
SSG with an uncached external API. Build fails or times out on Vercel when the external service is slow or rate-limits. Fix: add { next: { revalidate: false } } to cache the fetch permanently at build time, or use mock data for the build and ISR for runtime updates.
Real-world usage
- Next.js 14 Vercel Commerce template: ISR with
revalidatefor product pages, SSG for category and landing pages. - Gatsby: SSG for Netflix blog and large documentation sites, static export to S3 + CloudFront.
- Remix: SSR for Shopify Hydrogen (dynamic carts, user sessions, personalized recommendations).
- Nuxt 3: universal SSR and SSG for Vue apps, used in Nuxt Content docs.
- SvelteKit:
prerender = truefor static routes, SSR for dynamic ones in the same app.
I've seen teams default to SSR for everything and then wonder why server costs spike. The fix is almost always moving pages that don't need per-request freshness to ISR or SSG.
Follow-up questions
Q: How does hydration work in SSR, and what causes a mismatch?
A: React compares the server-generated HTML with what a client-side render would produce. If they differ, React logs a warning and re-renders the component tree from scratch in the browser. Common causes: timestamps, Math.random(), window access, or any value that differs between server and client environments.
Q: What are realistic LCP numbers for each strategy?
A: SSG and ISR served from a CDN typically hit under 100ms LCP. SSR lands at 200-500ms depending on server location and database latency. CSR averages 2-5 seconds on typical connections because the browser has to download, parse, and execute the JS bundle before anything renders.
Q: How does ISR handle a traffic spike during revalidation?
A: The cached page keeps serving to all users while the background rebuild runs. Only one rebuild fires at a time regardless of how many requests come in. Once the new build finishes, it replaces the cache atomically, so there is no gap where users see a partially built page.
Q: What is the difference between ISR and Partial Prerendering (PPR) in Next.js 15?
A: ISR rebuilds the whole page on a timer or webhook trigger. PPR (experimental in Next.js 15) prerenders a static shell at build time and fills in dynamic sections at request time without rebuilding the whole page. For a product page where the description is static but the cart count is dynamic, PPR is faster because only the dynamic holes incur server compute.
Q: How does CSR compare to islands architecture?
A: Full CSR ships the entire JS bundle to the browser and renders everything client-side. Islands architecture (Astro, Fresh) prerenders the static shell as plain HTML, then hydrates only the interactive parts, like a search box or a cart widget. The rest stays as HTML with no JS attached. The result is a smaller JS payload, faster time to interactive, and better SEO than CSR with a similar developer experience.
Examples
CSR: client component fetching user data
// Vite + React - full CSR, no server rendering involved
import { useState, useEffect } from 'react';
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<{ name: string; email: string } | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>{user!.name} - {user!.email}</div>;
}
// What the server sends: <div id="root"></div>
// What the user sees first: nothing (or a spinner)
// SEO crawler sees: empty divThe server sends nothing useful. The browser downloads the JS bundle, executes it, fires the API call, waits for a response, and finally renders. This is the source of the blank-screen problem that SSR and SSG solve.
ISR: e-commerce product page with on-demand revalidation
// app/products/[id]/page.tsx - Next.js 14 App Router
export const revalidate = 60; // rebuild at most every 60 seconds
async function getProduct(id: string) {
const res = await fetch(
`https://api.example.com/products/${id}`,
{ next: { tags: ['product', `product-${id}`] } } // tag for targeted revalidation
);
return res.json();
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
<p>{product.inStock ? 'In stock' : 'Out of stock'}</p>
</div>
);
}
// app/api/revalidate/route.ts - CMS calls this webhook on publish
import { revalidateTag } from 'next/cache';
export async function POST(request: Request) {
const { productId } = await request.json();
revalidateTag(`product-${productId}`);
return Response.json({ revalidated: true });
}The page serves from cache in under 10ms. When the price changes in the CMS, the CMS calls the revalidation endpoint, Next.js rebuilds only that product's page in the background, and the updated version replaces the cache. No deploy required, no downtime.
SSR vs ISR: picking the right one for a product page
// Option A: SSR - fresh HTML on every single request
// app/products/[id]/page.tsx
async function getProduct(id: string) {
const res = await fetch(
`https://api.example.com/products/${id}`,
{ cache: 'no-store' } // bypasses all caching
);
return res.json();
}
// Response time: 200-500ms every request
// Good for: real-time stock, user-specific pricing, cart data
// Option B: ISR - cached, rebuilds every 60 seconds
export const revalidate = 60;
async function getProduct(id: string) {
const res = await fetch(
`https://api.example.com/products/${id}`,
{ next: { revalidate: 60 } }
);
return res.json();
}
// Response time: <10ms from CDN for 60 seconds, then one background rebuild
// Good for: product descriptions, prices that update a few times per dayIf the product price changes every few seconds and accuracy matters at the second level, SSR is the right call. If a 60-second window of potential staleness is acceptable and the page needs to handle thousands of requests per second, ISR is the better fit. Most product pages are fine with ISR and a CMS webhook for on-demand updates.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.