February 20, 2025
·4 min read
SSR vs SSG: A Practical Decision Guide for 2025
Stop guessing. Here's a framework for deciding when to use Server-Side Rendering, Static Generation, or ISR in Next.js — with real tradeoffs.
◆ The Eternal Debate
Every new Next.js project starts with the same question: SSR or SSG?
The documentation explains both. The internet has a thousand opinions. But most of them miss the actual decision logic that matters in production.
Here's the framework I use on every project.
◆ The Core Question
Before picking a rendering strategy, ask this:
How stale is "too stale" for this data?
That question maps almost directly to your rendering approach.
◆ The Decision Matrix
| Staleness tolerance | Strategy | Next.js approach |
|---|---|---|
| Real-time (< 1s) | SSR | export const dynamic = 'force-dynamic' |
| Minutes to hours | ISR | export const revalidate = 3600 |
| Hours to days | SSG with revalidate | generateStaticParams + revalidate |
| Static forever | SSG | generateStaticParams, no revalidate |
◆ When to Use SSR
Use SSR when:
- Data is personalized per user — cart contents, account info, localized pricing
- Data changes faster than your revalidation window — stock levels, live scores, auction prices
- Request context matters — cookies, auth headers, A/B test assignments
// Opt into SSR for this page
export const dynamic = 'force-dynamic';
export default async function CartPage() {
const cart = await getCart(); // always fresh
return <CartView items={cart.items} />;
}
The tradeoff: every request hits your server. You pay in latency and compute. Worth it when freshness is non-negotiable.
◆ When to Use SSG
Use SSG when:
- Content is shared across all users — blog posts, marketing pages, docs
- Data changes infrequently — product descriptions, team bios, pricing tiers
- Maximum performance is the goal — pre-rendered HTML served from CDN edge
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map(p => ({ slug: p.slug }));
}
// No revalidate = static until next deploy
A static page served from a CDN edge will always beat a server-rendered equivalent. This is the physics of the web.
◆ The ISR Sweet Spot
ISR is the most underused strategy. It gives you:
- Pre-built static pages (fast first load)
- Automatic revalidation after a time window
- On-demand purging with cache tags
export const revalidate = 3600; // rebuild stale pages every hour
export default async function ProductPage({ params }) {
const product = await getProduct(params.slug);
return <ProductView product={product} />;
}
For e-commerce, ISR is usually the right answer. Product pages are mostly static — but inventory, price, and availability need to update.
◆ React Server Components Change the Game
With RSC, you can mix strategies at the component level:
// This page is statically generated...
export const revalidate = 3600;
export default async function ProductPage({ params }) {
const product = await getProduct(params.slug); // static, cached
return (
<div>
<ProductDetails product={product} /> {/* static RSC */}
<Suspense fallback={<StockSkeleton />}>
<LiveStockLevel sku={product.sku} /> {/* dynamic RSC */}
</Suspense>
</div>
);
}
The page skeleton is static and fast. The live stock level streams in dynamically. You get the best of both worlds.
◆ The Rule I Follow
Default to SSG. Add ISR when data changes. Add SSR only when you truly need real-time or per-user data.
Most sites should be mostly static. The web was built on static. Static is fast, cheap, and resilient.
Every time you add force-dynamic, you're making a conscious tradeoff. Make that tradeoff deliberately.
◆ Conclusion
The SSR vs SSG debate is usually the wrong question. The right question is:
"What does this specific piece of content need?"
Answer that, apply the matrix, and you'll pick the right strategy almost every time.