Rendering Strategies: SSR, CSR, SSG, and ISR
Concept
Every page in a web app has to answer the same question — where and when does its HTML get built? — and the four classic answers trade off against each other along the same three axes every time: how fresh the data is, how fast the first byte/paint arrives, and how much it costs to serve at scale.
- CSR (Client-Side Rendering) — the server sends a near-empty HTML shell plus a JS bundle; the browser fetches data and renders everything after load. Cheapest to host (a static bundle behind a CDN), but the user stares at a blank screen or skeleton until JS downloads, parses, and fetches data — bad for both perceived performance and SEO on first load, since crawlers historically struggle with content that only exists after client-side JS executes.
- SSR (Server-Side Rendering) — HTML is rendered fresh on every request, on a server (or serverless function) that has direct access to the data. The page appears fully formed on load, and SEO/social-preview crawlers see real content — but every single request pays a server round-trip (often including a database call) before the first byte can be sent, and every request consumes compute.
- SSG (Static Site Generation) — HTML is rendered once, at build time, and served as a static file thereafter. Fastest possible response (a CDN edge cache serves it, no server compute per request) and cheapest to run, but the content is frozen until the next build/deploy — wrong for anything that changes between deploys.
- ISR (Incremental Static Regeneration) — a hybrid: pages are pre-built like SSG, but a background revalidation window lets a stale page regenerate after a configured interval without a full rebuild, so pages stay mostly static-fast while tolerating periodic content changes.
Frameworks with a modern app router (Next.js's App Router is the reference example) let this decision be made per-route rather than per-app — a single deployment can serve a marketing page as SSG, a product listing as ISR, and an authenticated dashboard as CSR, simultaneously. That per-route granularity is the real lever; picking one strategy for an entire application is almost always leaving performance or cost on the table somewhere.
Tradeoffs
| Strategy | When HTML is built | Freshness | Cost at scale | SEO/first-paint quality |
|---|---|---|---|---|
| CSR | In the browser, after JS loads | Always current (client fetches live) | Cheapest hosting (static bundle) | Weak — content arrives after a blank/skeleton state |
| SSR | Per request, on the server | Always current | Most expensive — every request is a compute invocation, often hitting a DB | Strong — full HTML on first response |
| SSG | Once, at build time | Stale until next deploy | Cheapest to run — pure CDN-cached static files | Strong, and fastest possible TTFB |
| ISR | At build time, then periodically regenerated in the background | Bounded staleness (as fresh as the revalidation window) | Near-SSG cost, occasional regeneration compute | Strong, near-SSG speed |
Newer hybrid approaches (Next.js's Partial Prerendering) push this further by making the unit of rendering the region within a page rather than the whole route — a static shell ships instantly from the CDN while a personalized or live-data region streams in separately — which is the framework-level answer to teams who kept hitting the same wall of "I want SSG's speed and SSR's freshness on the same page."
When to use / when not to
- Use SSG for content that changes only at deploy time — marketing pages, documentation, most blog content.
- Use ISR for content that updates periodically but doesn't need per-request freshness — product catalogs, listing pages, news aggregation — where "correct as of the last few minutes" is an acceptable tradeoff for CDN-level speed.
- Use SSR for pages that must reflect the exact current state on every load and can't tolerate any staleness — personalized or auth-gated views (an admin dashboard reading live data, a page whose content depends on the requesting user).
- Use CSR for content that's already behind authentication and has no SEO requirement — most interactive, logged-in-only surfaces (settings panels, in-app dashboards, chat) where the user has already paid the cost of loading the app shell once.
- Don't default an entire app to SSR "to be safe" — pages with no per-request personalization pay full server-compute cost for freshness nobody needed; ISR or SSG gets the same visible result far cheaper.
- Don't SSG or ISR anything whose content is itself personalized per visitor — a stale, shared build can leak the wrong user's cached data into someone else's page if the revalidation/caching boundary isn't scoped correctly.
Common pitfall
Treating rendering strategy as a single, app-wide decision made once at project setup, instead of a per-route choice revisited as each page's actual requirements become clear. The most common concrete version: reaching for SSR "for SEO" on pages that don't actually need per-request freshness, which quietly turns every page load into a billed serverless invocation and a database round-trip — at real traffic volumes this can shift a site from flat, predictable static-hosting cost to compute cost that scales linearly (sometimes non-linearly, under cold starts) with visitors. The fix is rarely "switch everything to one strategy" — it's auditing each route for whether it actually needs per-request data, and downgrading to ISR or SSG wherever it doesn't.
Engineering Lens
Rendering strategy and Core Web Vitals aren't separate concerns — they're the same problem viewed from different angles: SSG/ISR win LCP and TTFB by moving work out of the request path entirely, while CSR's biggest weakness is exactly the "blank page waiting on JS + data" window Core Web Vitals' LCP metric is built to penalize. The transferable habit is to ask, for every route, "what's the actual staleness tolerance here?" before picking a strategy — a page that can tolerate being a few minutes old (ISR) should never be paying SSR's per-request compute bill just because SSR was the safer-sounding default.
Sources
- How to choose the best rendering strategy for your app — Vercel
- SEO: Rendering Strategies — Next.js
- Unlocking Performance with Partial Prerendering in Next.js — Leapcell