CSR vs SSR vs SSG vs ISR Compared
The One Question That Changes Everything
Every rendering strategy answers the same question: when does HTML get generated?
- At build time? (SSG)
- Per request on the server? (SSR)
- In the browser after JS loads? (CSR)
- At build time, then refreshed in the background? (ISR)
That single difference — when — cascades into everything: performance, SEO, caching, infrastructure cost, and user experience. Once you internalize this mental model, picking the right strategy for any page becomes trivial.
Think of a restaurant. SSG is a buffet — food is prepared ahead of time and served instantly. SSR is a made-to-order kitchen — each dish is cooked when you order, fresher but slower. CSR is a meal kit — you get raw ingredients (JavaScript) and cook it yourself in your browser. ISR is a buffet that quietly replaces dishes in the background when they get stale — you always get served instantly, but the food stays reasonably fresh.
Client-Side Rendering (CSR)
The server sends a nearly empty HTML file. Your browser downloads a JavaScript bundle, executes it, and React builds the entire DOM from scratch.
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="/bundle.js"></script>
</body>
</html>
The user sees a blank screen until JS downloads, parses, and executes. Then the app renders, fetches data, and renders again.
Server sends empty HTML
↓
Browser downloads JS bundle (200KB+)
↓
JS parses and executes
↓
React mounts, renders DOM
↓
Data fetching begins
↓
Re-render with data
↓
User finally sees content
The waterfall is brutal. On a slow 3G connection, users can stare at a white screen for 5-10 seconds.
When CSR works: Internal dashboards, authenticated apps where SEO doesn't matter, highly interactive tools (think Figma). The tradeoff is acceptable when your users are on fast connections and the page is behind a login.
When CSR fails: Marketing pages, blogs, e-commerce — anywhere first-paint speed and SEO matter.
Server-Side Rendering (SSR)
The server runs your React components on every request and sends fully-formed HTML. The browser gets real content immediately, then hydrates it with JavaScript to make it interactive.
User requests page
↓
Server runs React components
↓
Server fetches data (DB, APIs)
↓
Server renders full HTML
↓
Browser receives HTML (content visible!)
↓
JS downloads and hydrates
↓
Page becomes interactive
The key insight: the user sees content before JavaScript loads. TTFB is slower (the server does work), but First Contentful Paint is faster because the browser renders HTML immediately — no waiting for JS.
export default async function ProductPage({ params }) {
const product = await db.product.findUnique({
where: { slug: params.slug }
})
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCartButton productId={product.id} />
</main>
)
}
In Next.js App Router, every component is server-rendered by default. No configuration needed.
The tradeoff: Every request hits your server. No CDN caching (the response depends on the request). Server costs scale linearly with traffic.
Static Site Generation (SSG)
HTML is generated once at build time. Every user gets the same pre-built HTML, served directly from a CDN.
export async function generateStaticParams() {
const posts = await db.post.findMany({ select: { slug: true } })
return posts.map((post) => ({ slug: post.slug }))
}
export default async function BlogPost({ params }) {
const post = await db.post.findUnique({
where: { slug: params.slug }
})
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
At build time, Next.js calls generateStaticParams, gets all slugs, renders each page, and outputs static HTML files. These get deployed to a CDN edge network.
Build time: Generate HTML for every page
↓
Deploy to CDN (200+ edge locations)
↓
User requests page
↓
CDN serves pre-built HTML instantly
↓
TTFB: ~20ms (edge-cached)
When SSG shines: Blog posts, documentation, marketing pages, course content — anything where the data changes infrequently and pages are the same for all users.
When SSG breaks: Pages that depend on the request (user-specific data, search results) or content that changes every few minutes. You'd need to rebuild and redeploy for every change.
Incremental Static Regeneration (ISR)
ISR is SSG with a refresh mechanism. Pages are statically generated at build time, but they can be regenerated in the background after a configurable time interval.
export const revalidate = 3600
export default async function PricingPage() {
const prices = await fetchPrices()
return (
<main>
<h1>Pricing</h1>
{prices.map(price => (
<PriceCard key={price.id} {...price} />
))}
</main>
)
}
The revalidate = 3600 tells Next.js: serve the cached static page, but if it's older than 1 hour and someone requests it, regenerate it in the background. The current visitor gets the stale page instantly. The next visitor gets the fresh one.
This is the stale-while-revalidate pattern. Users always get instant responses, and content stays reasonably fresh.
Request 1: Serve static HTML (cached)
Request 2 (after revalidate period): Serve stale HTML, trigger background regen
Request 3: Serve fresh HTML (regenerated)
Next.js also supports on-demand revalidation — you can trigger a rebuild for specific pages via revalidateTag or revalidatePath from a webhook:
import { revalidatePath } from 'next/cache'
export async function POST(request: Request) {
revalidatePath('/blog')
return Response.json({ revalidated: true })
}
The Comparison
| Factor | CSR | SSR | SSG | ISR |
|---|---|---|---|---|
| HTML generation | Browser (runtime) | Server (per request) | Build time | Build + background regen |
| TTFB | Fast (empty HTML) | Slower (server work) | Fastest (CDN) | Fastest (CDN) |
| First Contentful Paint | Slow (JS required) | Fast (real HTML) | Fastest (pre-built) | Fastest (pre-built) |
| Time to Interactive | Slow | Moderate (hydration) | Moderate (hydration) | Moderate (hydration) |
| SEO | Poor (empty HTML) | Excellent | Excellent | Excellent |
| Dynamic data | Excellent | Excellent | None (build-time only) | Good (stale-while-revalidate) |
| Personalization | Excellent | Excellent | None | Limited |
| CDN cacheable | JS bundle only | No (per-request) | Fully | Fully |
| Server cost | Minimal | High (per request) | Build only | Low |
| Build time | Fast | N/A | Slow (scales with pages) | Moderate |
The Decision Framework
Here is the thing most people miss: you don't pick one strategy for your entire app. In Next.js App Router, you pick per route.
Mixing strategies in Next.js App Router
In the App Router, rendering strategy is determined by your data access patterns, not a page-level configuration flag. If a Server Component awaits a database call with no caching, it's SSR. If you add export const revalidate = 60, it becomes ISR. If all data is fetched at build time via generateStaticParams, it's SSG. And Client Components that fetch in useEffect are CSR. A single page can mix all four — the product description is SSG, the price is ISR, the user's cart icon is CSR, and the recommendations are SSR.
| What developers do | What they should do |
|---|---|
| Use CSR for everything because it is simpler CSR sends empty HTML, destroying SEO and first-paint performance. Most pages benefit from server-generated HTML. | Use SSR/SSG by default, CSR only for authenticated interactive features |
| Rebuild entire site for every content change with SSG ISR gives you SSG performance with near-realtime content updates. No full rebuild needed. | Use ISR with on-demand revalidation for content that changes |
| Use SSR for static content like blog posts SSR re-renders on every request, wasting server resources and adding latency for content that rarely changes. | Use SSG or ISR for content that is the same for every user |
| Assume SSR is always better than CSR SSR adds server cost and complexity. For internal tools where SEO is irrelevant, CSR is perfectly fine. | Use CSR for highly interactive, personalized dashboards behind auth |
FAANG interview question: "You're building an e-commerce site with 50,000 product pages. The product descriptions change rarely, but prices update every 5 minutes. How would you architect the rendering strategy?"
Strong answer: Use ISR for product pages with a 5-minute revalidation interval. Product descriptions and images are static (great for CDN caching), while prices get refreshed in the background. For the cart and user-specific elements, use Client Components that fetch from an API. For the search results page, use SSR since every query produces unique results. This gives you CDN-speed page loads for the 99% case while keeping prices reasonably fresh.
- 1CSR: browser generates HTML after JS loads. Fast TTFB, terrible FCP, no SEO.
- 2SSR: server generates HTML per request. Great FCP and SEO, but no CDN caching and higher server costs.
- 3SSG: HTML generated at build time. Best performance (CDN-served), but content is frozen until rebuild.
- 4ISR: SSG with background regeneration. CDN performance with configurable freshness via stale-while-revalidate.
- 5Pick the strategy per route, not per app. Most apps use a mix of all four.
- 6Default to SSG/ISR for public content, SSR for personalized server-rendered pages, CSR for interactive widgets behind auth.