I had a senior dev ask me "what rendering strategy would you use here?" and I said "...SSR?" with zero confidence. I'd heard the acronyms everywhere but never actually understood the tradeoffs.
This is the explanation I wish someone had given me back then — concrete, with real examples, no hand-waving.
The one question that unifies everything
Every rendering strategy is answering the same question: when and where does your HTML get built?
- On your server, fresh for every request? → SSR
- In the user's browser after downloading JavaScript? → CSR
- Once at build time, before any user ever visits? → SSG
- Built at deploy time but refreshed in the background? → ISR
That's it. Every performance, SEO, and cost tradeoff follows from this one decision.
Server-Side Rendering (SSR)
Your server builds the HTML fresh on every single request.
User clicks link
→ Request hits your server
→ Server queries the database
→ HTML is built with fresh data
→ Browser gets a complete page
→ User sees content instantly
This is the OG approach. PHP, Ruby on Rails, Django — all of these work this way by default.
// Next.js SSR
export async function getServerSideProps({ params }) {
// This runs on the SERVER for every request
const product = await db.products.findOne(params.id);
const inventory = await db.inventory.getCurrent(params.id);
return { props: { product, inventory } };
}
export default function ProductPage({ product, inventory }) {
return (
<div>
<h1>{product.name}</h1>
<p style={{ color: inventory.count === 0 ? 'red' : 'green' }}>
{inventory.count === 0 ? 'Out of stock' : `${inventory.count} remaining`}
</p>
</div>
);
}
Every page load hits your database. Inventory is always accurate. User always gets the real-time truth.
When SSR is the right call:
- Search results (every query is different, can't cache)
- User-specific pages ("your orders", account settings)
- Real-time data (live auction prices, active game scores)
- Anything where stale data causes real problems
The honest tradeoff: If 20,000 people hit the same page at the same time, your server handles 20,000 individual database queries. That's expensive infrastructure to get right, and you feel it in your cloud bill.
Client-Side Rendering (CSR)
Your server serves an almost empty HTML file. The browser downloads JavaScript, runs it, calls APIs, and builds the page itself.
User clicks link
→ Server sends: <div id="app"></div> + bundle.js
→ Browser downloads ~300KB of JavaScript
→ JavaScript parses and executes
→ API calls go out to fetch data
→ React renders the DOM
→ User finally sees something (2-4 seconds later)
This is the default behavior of Create React App. No framework magic, just a browser doing all the work.
// Pure CSR — everything happens in the browser
export default function Dashboard() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/dashboard-stats')
.then(r => r.json())
.then(data => {
setData(data);
setLoading(false);
});
}, []);
if (loading) return <DashboardSkeleton />;
return <DashboardGrid data={data} />;
}
// No getServerSideProps. No getStaticProps. Just browser logic.
Server barely breaks a sweat — it just delivers a static file. The user's device does all the rendering work.
When CSR makes sense:
- Behind a login (SEO doesn't matter if only authenticated users see it)
- Highly interactive tools — Figma, Notion, Gmail-style apps
- Real-time collaborative features
- Admin dashboards and internal tooling
The honest tradeoff: Google crawls your page and sees <div id="app"></div>. No content. No SEO. That blank white flash while JavaScript loads is also genuinely bad UX on slow connections.
Static Site Generation (SSG)
You build every page once at deploy time. Ship the HTML files to a CDN. Done.
npm run build (runs once at deploy)
→ Framework reads all your content
→ Generates one complete HTML file per page
→ Files go to CDN edge servers worldwide
User requests any page later
→ CDN returns pre-built HTML in ~50ms
→ No server touched. No database hit.
// Next.js SSG — data fetched at BUILD TIME, not request time
export async function getStaticPaths() {
const posts = await getAllBlogPosts();
return {
paths: posts.map(p => ({ params: { slug: p.slug } })),
fallback: false
};
}
export async function getStaticProps({ params }) {
const post = await getBlogPost(params.slug); // Runs ONCE at build
return {
props: { post }
// No revalidate = frozen until next deploy
};
}
export default function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Build once, serve for free, to millions of people. For a blog or documentation site, this is genuinely hard to beat.
When SSG is the right call:
- Blogs and personal sites
- Documentation sites
- Marketing and landing pages
- Content that changes weekly at most
The honest tradeoff: Content is frozen in time. Fix a typo? Redeploy. Have 10,000 products with prices that update hourly? You're rebuilding the entire site every hour — a 15-minute deploy cycle just to update a dollar amount. That's where SSG breaks down.
Incremental Static Regeneration (ISR)
This is the one that made me stop and re-read the docs.
ISR gives you SSG's speed (CDN delivery) but lets individual pages regenerate in the background without rebuilding everything. Fast serving AND fresh data.
Normal flow (page is cached):
User request → CDN returns pre-built HTML → ~50ms
When data changes:
Database update
→ Call revalidatePath('/products/123')
→ Server regenerates just that one page (~2 seconds)
→ Next user gets fresh HTML from CDN → still ~50ms
// Next.js ISR — static but refreshable
export async function getStaticProps({ params }) {
const product = await getProduct(params.id);
return {
props: { product },
revalidate: 3600 // Rebuild every hour OR trigger manually
};
}
export async function getStaticPaths() {
// Pre-build your 100 most visited products at deploy time
const topProducts = await getTopProducts(100);
return {
paths: topProducts.map(p => ({ params: { id: p.id } })),
fallback: 'blocking' // Others: generate on first request, cache after
};
}
And the on-demand version — this is what makes it genuinely powerful:
// Called from your webhook when inventory changes
async function onInventoryUpdate(productId, newCount) {
await db.inventory.update(productId, newCount);
// Tell Next.js to regenerate this specific page only
await fetch(`${process.env.SITE_URL}/api/revalidate`, {
method: 'POST',
body: JSON.stringify({ path: `/products/${productId}` }),
});
}
// pages/api/revalidate.js
export default async function handler(req, res) {
await res.revalidate(req.body.path);
return res.json({ revalidated: true });
}
Inventory changes → webhook fires → that one product page regenerates → next visitor gets accurate stock count, still from CDN in 50ms.
When ISR is the sweet spot:
- E-commerce with hundreds or thousands of product pages
- News sites where articles get updates but aren't live
- Real estate listings, job boards, classified ads
- Any content with too many pages for full rebuilds but not so volatile it needs SSR
The honest tradeoff: The first visitor to an uncached page waits like SSR (1-2 seconds). And ISR isn't right if your data changes every few seconds — at that volatility, just use SSR.
Hybrid (What production apps actually do)
Nobody sane uses one strategy for their whole app. The smart move is matching each page to what it actually needs.
Here's a realistic e-commerce breakdown:
| Route | Strategy | Why |
|---|---|---|
/ |
SSG | Homepage barely changes |
/products/:id |
ISR | Regenerate when inventory updates |
/search?q=shoes |
SSR | Every query is unique, can't cache |
/blog/:slug |
SSG | Posts don't change after publish |
/account/orders |
SSR | User-specific, must be current |
/cart |
CSR | Client-side state, no server needed |
In Next.js, this is just different files using different get*Props functions. One codebase, multiple strategies.
The cost profile ends up roughly:
- 75% of traffic → CDN (nearly free, instant)
- 20% → server (search results, auth pages)
- 5% → browser-rendered (cart, settings, dashboard)
Compared to all-SSR: dramatically cheaper, faster for users, and your server isn't the bottleneck anymore.
How I actually decide
When someone asks which strategy to use for a given page, here's the actual thought process:
Does SEO matter for this page?
No → CSR. Done.
Is the content personalized per user?
Yes → SSR. You can't cache personal data.
How often does the data actually change?
Every second → SSR
Every hour/day → ISR
Every week or less → SSG
How many pages are there?
Under 500 → SSG is easy, builds are fast
Over 500 → ISR, or your builds become a scheduling problem
For most content sites this shakes out to: SSG for marketing and docs, ISR for product and content pages, SSR for search and user-specific pages, CSR for authenticated dashboards.
Closing thought
The thing that finally clicked for me was stopping thinking about this as a framework choice and starting to think about it as a question about data.
When does your data change? Who sees it? How bad is it if a user sees something 10 minutes stale?
Answer those honestly and the right strategy usually picks itself. Next.js, Remix, Astro — they've all made hybrid rendering easy. You're not locked into one approach for your whole codebase.
SSR for things that must be live. SSG for things that never change. ISR for everything in between. CSR for pages behind a login where interactivity matters more than SEO.
Once that mental model is in place, the acronyms stop being confusing jargon and start being genuinely useful tools.
Top comments (0)