SSR vs SSG vs ISR: Choosing the Right Rendering Strategy for React
Overview
Rendering strategy is one of the most consequential architecture decisions in a React project. Get it right and your Largest Contentful Paint drops below 1 second. Get it wrong and you are fighting framework complexity for years. This post breaks down when to use each approach.
Problem
A single-page React app (SPA) ships an empty HTML shell. The browser downloads JavaScript, executes it, fetches data, and then renders content. Google's crawler waits for this — up to a point. But Core Web Vitals suffer because the user stares at a blank screen.
Solutions exist: pre-render the HTML. But the three approaches — SSR, SSG, and ISR — have dramatically different trade-offs.
Solution
Static Site Generation (SSG)
How it works: HTML is generated once at build time. Every user gets the same pre-built file.
Build time: Fetch data → Render React → Write HTML file
Request: CDN serves static HTML → Browser hydrates with JS
When to use it:
- Content changes infrequently (blog posts, docs, marketing pages)
- Page count is manageable (under ~10,000 pages)
- Fastest possible TTFB is critical
Real performance:
- TTFB: 10–50ms (served from CDN edge)
- LCP: 0.5–1.2s (content is in the HTML)
- Build time: scales linearly with page count
The problem: When content changes, you rebuild and redeploy. For a blog with 50 posts this takes seconds. For an e-commerce site with 100,000 products, it takes hours.
Server-Side Rendering (SSR)
How it works: HTML is generated on every request. The server fetches data, renders React, and streams the result.
Request: Server receives request → Fetch data → Render React → Send HTML
Browser receives HTML → Hydrates with JS
When to use it:
- Content is user-specific (dashboards, authenticated pages)
- Data changes frequently (real-time prices, inventory)
- SEO is critical AND content is dynamic
Real performance:
- TTFB: 100–500ms (depends on data fetching + render time)
- LCP: 1.0–2.5s (HTML arrives later but is complete)
- No build-time scaling issue
The problem: Every request requires server compute. Cold starts on serverless platforms add 500ms+. Caching helps, but then you are reinventing SSG.
Incremental Static Regeneration (ISR)
How it works: Pages are generated statically, but can be re-generated in the background after a configurable interval.
First request: Serve stale static HTML → Trigger background regeneration
Second request: Serve newly generated HTML
When to use it:
- Content updates are frequent but not real-time
- You want SSG performance with SSR freshness
- Large page counts make full rebuilds impractical
Real performance:
- TTFB: 10–50ms (same as SSG — serving static files)
- LCP: 0.5–1.2s
- Staleness: configurable (60s, 5min, 1hr)
The problem: You accept serving potentially stale content. Not acceptable for prices, inventory, or anything where a user might act on outdated information.
Implementation
Decision Matrix
| Factor | SSG | SSR | ISR |
|---|---|---|---|
| TTFB | Best | Worst | Best |
| Content freshness | Build-time only | Real-time | Near real-time |
| Server cost | Zero (CDN) | Per-request | Minimal |
| Build time | Scales with pages | None | None |
| SEO | Excellent | Excellent | Excellent |
| Personalization | None | Full | Limited |
Hybrid Approach (What Works Best)
Most production apps should not pick one strategy — they should mix them:
- SSG for marketing pages, docs, blog index
- ISR for individual blog posts (revalidate every 5 min)
- SSR for authenticated dashboards, search results
- CSR (client-side) for highly interactive UI after initial load
This is exactly how this portfolio works:
- Homepage: SSG with pre-rendered HTML injected at build time
- Blog posts: SSG with content fetched from Supabase at build
- Admin dashboard: CSR behind authentication
SSG Without a Framework
You do not need Next.js for SSG. A Vite + React SPA can achieve the same result with a build-time prerender script:
// scripts/prerender.ts
const routes = ["/", "/blog", "/contact"];
for (const route of routes) {
const html = injectMetaTags(template, routeMetadata[route]);
const staticContent = generateStaticHTML(route);
const final = html.replace(
'<div id="root"></div>',
`<div id="root">${staticContent}</div>`
);
writeFileSync(`dist${route}/index.html`, final);
}
React's createRoot replaces the static content on hydration. Crawlers see full HTML. Users see content immediately.
Challenges
Hydration mismatch: If your SSG HTML does not match what React renders on the client, you get hydration errors. Common causes: date formatting, random IDs, browser-only APIs. Solution: use deterministic rendering and guard browser APIs with typeof window checks.
Cache invalidation with ISR: When you update a blog post, the old version might be served for up to revalidate seconds. For critical content, use on-demand revalidation (webhook triggers rebuild of specific pages).
Conclusion
SSG is the right default for content sites. It is the fastest, cheapest, and simplest. Add ISR when content freshness matters. Reach for SSR only when personalization or real-time data is required. Most apps need a hybrid — not a single strategy.