SSR vs SSG: Choosing the Right Rendering Strategy
Each rendering strategy—SSR, SSG, ISR—has trade-offs. The right choice depends on content freshness, scale, and budget.
Static Generation (SSG)
Pre-build pages at deploy time. Pages are static HTML served from CDN.
// Next.js
export async function getStaticProps() {
const posts = await fetch('https://api.example.com/posts')
.then(r => r.json())
return {
props: { posts },
revalidate: 3600, // ISR: revalidate every hour
}
}
Advantages:
- Speed: Serve from CDN, minimal latency.
- Scale: Millions of concurrent users; CDN handles load.
- Cost: Cheap—no servers needed.
- SEO: Full HTML at request time; crawlers happy.
Disadvantages:
- Build time: Rebuilding 1M pages takes hours.
- Freshness: Content stale until rebuild.
- Dynamic content: Hard to personalize per-user.
Best for: Blogs, marketing sites, documentation. Content changes infrequently.
Server-Side Rendering (SSR)
Render pages on-demand on the server.
// Next.js
export async function getServerSideProps() {
const posts = await fetch('https://api.example.com/posts')
.then(r => r.json())
return { props: { posts } }
}
Advantages:
- Freshness: Always current.
- Dynamic content: Easy to personalize per-user.
- Flexibility: No build step constraints.
Disadvantages:
- Latency: Server must render every request (100-500ms overhead).
- Cost: Server infrastructure required.
- Scale: Hard to scale beyond a few requests/second without caching.
Best for: User dashboards, real-time data, personalized content.
Incremental Static Regeneration (ISR)
Hybrid: generate static pages, revalidate on-demand.
export async function getStaticProps() {
const posts = await fetch('https://api.example.com/posts')
.then(r => r.json())
return {
props: { posts },
revalidate: 60, // Revalidate every 60 seconds
}
}
How it works:
- User requests
/blog/post-1. - Server checks: Is the cached version older than 60 seconds?
- If fresh, serve cached version instantly.
- If stale, regenerate in background, serve old version immediately.
- Next user gets fresh version.
Advantages:
- Best of both: CDN speed + fresh content.
- On-demand revalidation: Webhook from CMS → trigger revalidate → next user sees fresh content.
- Scales well: Cache hits are fast; regeneration happens in background.
Disadvantages:
- More complex to set up.
- Requires understanding cache semantics.
Best for: Blogs, news sites, product catalogs. Content changes occasionally.
Dynamic Routes with SSG
Large sites with millions of dynamic routes:
// Generate ~1000 most popular pages at build time
export async function getStaticPaths() {
const topPosts = await fetch('https://api.example.com/posts/top?limit=1000')
.then(r => r.json())
return {
paths: topPosts.map(p => ({ params: { slug: p.slug } })),
fallback: 'blocking', // Others render on-demand (SSR)
}
}
fallback options:
false: Only pre-generated paths work. 404 for others.'blocking': Pre-generate on-demand, then cache. User waits.true: Serve stale version immediately, regenerate in background.
Comparison Table
| Feature | SSG | SSR | ISR |
|---|---|---|---|
| Speed | Fastest | Medium | Fast |
| Freshness | Stale | Always fresh | Fresh within TTL |
| Build time | Slow | No build | Medium |
| Cost | Cheapest | Expensive | Cheap |
| Scalability | Unlimited | Limited | High |
| Complexity | Low | Low | Medium |
Practical Strategy
Marketing site: SSG. Content rarely changes. Build once, serve forever.
Blog: ISR. New posts a few times a week. Revalidate on publish.
SaaS dashboard: SSR. All content is user-specific. Cache aggressively server-side.
E-commerce: Hybrid. Product listing pages use ISR. User cart/checkout use SSR.
Caching Headers
Combine rendering strategies with HTTP caching:
// SSG page: Cache forever
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
// SSR page: Cache for 60 seconds
res.setHeader('Cache-Control', 'public, max-age=60')
// Private (user-specific): Don't cache
res.setHeader('Cache-Control', 'private, no-cache')
ISR In Practice
CMS publishes a blog post → webhook to /api/revalidate?secret=token&slug=new-post → Next.js revalidates /blog/new-post → next request gets fresh version.
// pages/api/revalidate.js
export default async function handler(req, res) {
if (req.query.secret !== process.env.REVALIDATE_SECRET) {
return res.status(401).json({ message: 'Invalid token' })
}
await res.revalidate(`/blog/${req.query.slug}`)
res.json({ revalidated: true })
}
Simple, powerful, and scales incredibly well.
Frequently asked questions
Which is fastest: SSG or SSR?
SSG. Pre-built pages served from CDN are instantaneous. SSR requires server render time (100-500ms). ISR combines both.
Can I use ISR for pages with user-specific content?
No. ISR caches pages globally. User-specific content needs SSR or client-side fetch.
How long should I set revalidate timeout for ISR?
Depends on content freshness needs. Blog: 3600s (1 hour). News: 60s. Fast-changing: 5-10s. Balance freshness vs. cache hit rate.