Web / Architecture
SSR vs SSG vs ISR vs CSR: Web Rendering Strategies Compared
CSR builds the page in the browser from a JavaScript bundle, SSG renders HTML once at build time and serves it from a CDN, SSR renders HTML on every request on the server, and ISR renders HTML on the server but caches it and regenerates it in the background on a timer or on demand. The right choice is driven by one variable — how often the underlying data changes — and most production sites end up mixing strategies per route rather than picking a single one.
The Four Strategies, Defined
Rendering strategy answers one question: when and where does JavaScript become HTML? Everything else — SEO, performance, cacheability, cost — follows from the answer.
Client-Side Rendering (CSR)
The server sends a minimal HTML shell plus a JavaScript bundle; the browser downloads the bundle, executes it, calls APIs, and paints the page. Nothing meaningful exists in the initial HTML response. CSR was the default shape of SPAs built with Create React App, Vite, and early Next.js pages that fetched everything in useEffect. Its strengths are interactivity-heavy apps behind a login and the simplest possible hosting story — a static file server or CDN is enough. Its weaknesses are SEO for public content, time-to-first-meaningful-paint on slow devices, and the blank loading state between shell and hydration.
Static Site Generation (SSG)
HTML is produced at build time, route by route, and every request for that route is answered by a CDN with a file that already exists. Latency is a CDN round trip; origin cost is zero at request time; SEO is trivial because crawlers receive complete HTML. The constraint is freshness: content updates require a rebuild. SSG fits marketing pages, documentation, blogs, and any catalog that changes on a deploy schedule.
Server-Side Rendering (SSR)
HTML is produced on every request: the server runs your render function, queries the database, and streams a complete document. Freshness is perfect — the response always reflects current data — and personalization is trivial. The costs are latency (the user waits for your server and its data sources), origin compute on every hit, and the need to scale and cache the render itself. SSR fits dashboards, authenticated areas, feeds, and anything where the response is user-specific.
Incremental Static Regeneration (ISR)
ISR is SSG with a TTL. Pages are still static files served from the CDN, but each carries a revalidate window. When a request arrives for an expired page, the user gets the stale copy immediately while the server regenerates the fresh one in the background — stale-while-revalidate at the page level. The result: near-static latency, minutes-level freshness, and no full-site rebuild. On-demand variants let you invalidate by path or tag when specific data changes.
// Tagged fetch with on-demand revalidation
async function getPost(slug) {
const res = await fetch(apiUrl + `/posts/${slug}`, {
next: { revalidate: 360, tags: ["posts"] },
});
return res.json();
}
// Invalidate from a Route Handler or Server Action
import { revalidateTag } from "next/cache";
export async function POST(request) {
const { slug } = await request.json();
revalidateTag("posts");
return Response.json({ revalidated: true });
}Modern Hybrids: Dynamic Rendering and PPR
Next.js App Router introduced two ideas that blur the four classic categories. Dynamic rendering means a route is rendered on the server per request because something in it depends on request-time information — cookies(), headers(), searchParams, or an uncached fetch. It is not a setting you choose; it is what your code implies. You can override the inference with dynamic = "force-dynamic" or "force-static".
Partial Prerendering (PPR) is the hybrid that resolves the static-versus-dynamic tradeoff: prerender a static shell at build time, leave holes marked by <Suspense>, and stream dynamic content into those holes at request time. One route is simultaneously static and dynamic. The user gets CDN-speed HTML immediately and fresh data moments later.
// Partial Prerendering: static shell + streamed dynamic holes
import { Suspense } from "react";
export const experimental_ppr = true;
export default async function DashboardPage() {
return (
<Shell>
<StaticHeader /> {/* prerendered into the shell */}
<Suspense fallback={<ChartSkeleton />}>
<LiveChart /> {/* streams after the shell */}
</Suspense>
</Shell>
);
}Latency Profiles
Each strategy produces a different latency chain. Understanding the chain matters more than benchmark numbers, because the slowest link wins:
- CSR: network + bundle download + parse/execute + API round trip + client render. Two sequential network phases — the page cannot paint meaningful content until data returns.
- SSG: CDN edge latency only. No origin involvement, no data dependency at request time. The theoretical best.
- SSR: origin render time + your slowest synchronous data query + network. Repeatable only if you cache the output.
- ISR: CDN latency when fresh; CDN latency plus a background regeneration when stale. The regeneration never blocks the user.
The recurring SSR failure mode is a page that awaits a 400 ms third-party API before sending anything: every visitor pays that 400 ms, and under load it compounds as your render concurrency grows. ISR and PPR exist specifically to remove data latency from the critical path of the first byte.
SEO Implications
Modern Googlebot renders JavaScript, so CSR pages can be indexed — but rendering is queued behind a two-phase crawl, deferred resources may be missed, and non-Google engines (Bing, social scrapers, LLM crawlers used for AI answers) are far less reliable at executing JS. For public, indexable content, anything you can pre-render is strictly better.
SSG and ISR return fully formed HTML with metadata, Open Graph tags, and structured data in the first response — the best possible signal. SSR does the same per request, which matters for pages whose content must be current at crawl time: news, pricing, availability. CSR is acceptable for app shells and authenticated areas that should not be indexed anyway, and risky for landing pages you expect to rank.
A practical rule: if a URL is in your sitemap, it should return meaningful HTML without executing JavaScript.
Cache and Staleness Behavior
Caching is where strategies are actually decided. Four layers interact in a Next.js application: the browser cache, the CDN or edge cache, the framework data cache (memoized fetch results), and the client router cache (prerendered RSC payloads kept for instant back/forward navigation). Each has its own TTL, and they compose.
Staleness tolerance is the real decision variable:
- Zero staleness allowed — account balances, inventory at checkout, anything user-specific. SSR with no cache, or render on the client.
- Seconds to minutes acceptable — feeds, dashboards, search results. ISR with a short
revalidate, or SSR behind a CDN withs-maxageand stale-while-revalidate. - Hours acceptable — product catalogs, documentation, marketing pages. ISR with a long window plus on-demand revalidation by tag.
- Never changes without a deploy — terms, blog posts, landing pages. SSG with
revalidate: false.
// Static by explicit request — build-time HTML, no per-request render
export const dynamic = "force-static";
export default async function DocsPage() {
const doc = await fetch(apiUrl, { next: { revalidate: false } });
return <DocView doc={await doc.json()} />;
}// ISR at the route level — rebuild at most once per 60 seconds
export const revalidate = 60;
export default async function PricingPage() {
const pricing = await fetch(apiUrl + "/pricing", {
next: { revalidate: 3600 },
});
return <PricingTable data={await pricing.json()} />;
}
// Per-request: never cache this segment
export const dynamic = "force-dynamic";
export default async function AccountPage() {
const user = await getCurrentUser();
return <AccountDashboard user={user} />;
}Core Web Vitals Impact
Rendering strategy influences all three Core Web Vitals, but not in the obvious order of preference.
Largest Contentful Paint: SSG and ISR win by default — the hero is in the HTML the CDN returns. SSR can match it when the server renders quickly and the page is cacheable. CSR typically loses because LCP depends on JavaScript download, execution, and a second network waterfall for data; on a mid-tier phone over 4G that easily pushes LCP past the 2.5 s threshold.
Interaction to Next Paint: the dominant factor is how much JavaScript you ship and parse, not where HTML was made. A heavy CSR app with a large bundle will have worse INP than an SSR page with server components and almost no client JS. Note that a fast first paint can hide a bad INP: hydration work competing with the first user interaction is a classic SSR regression.
Cumulative Layout Shift: SSR and SSG reduce CLS when they reserve space with real content — but only if images carry dimensions and dynamic content is not injected above the fold. CSR often inflates CLS because placeholder skeletons are replaced by content of a different height. ISR/SSG with stable layouts are the safest CLS profile.
Decision Table: Data Update Frequency
Start from how often the content actually changes, then work outward to the strategy. This table covers the common cases; when in doubt, choose the least dynamic option that keeps content correct.
| Content | Update frequency | Strategy | Staleness bound |
|---|---|---|---|
| Marketing / landing pages | Per deploy | SSG | None (build-time) |
| Docs, blog, changelog | Daily to weekly | SSG or ISR (1h) | Minutes to hours |
| Product catalog, pricing | Hourly | ISR + on-demand tags | Minutes, event-driven |
| Search results, feeds | Minutes | SSR + edge cache | Seconds to minutes |
| Dashboards, analytics | Live | SSR + PPR shell | Per request |
| Authenticated accounts | Live, per user | SSR or CSR | Zero — never cache |
| Real-time trading / chat | Milliseconds | CSR + WebSockets | Zero — streaming |
| Admin tools, internal apps | User-driven | CSR | Zero — not indexed |
| Personalized marketing | Per session | Static shell + PPR hole | Shell cached, data live |
Real-World Fit: Who Uses What
Patterns recur across the industry, and they line up with the decision table:
- News and publishing — SSG or ISR for articles (updates are rare after publish), SSR for section fronts and search. A breaking-correction workflow uses on-demand revalidation rather than a rebuild.
- E-commerce — ISR for category and product pages (price/inventory revalidated by tag when a SKU changes), SSR for cart and checkout, CSR for the search-as-you-type overlay.
- SaaS dashboards — a PPR or SSR shell with authenticated data streamed via Suspense; nothing in the personalized region is ever cached across users.
- Documentation and marketing — pure SSG, rebuilt on content commits, served entirely from a CDN. This is the cheapest, fastest category of site to operate.
- Internal admin tools — CSR is correct. There is no SEO benefit, users are behind auth, and the interaction density justifies a full SPA runtime.
How Next.js Maps to These Strategies
In the App Router, rendering strategy is derived from your code rather than a single config flag. A route is static when nothing in it depends on request-time data; it becomes dynamic the moment you read cookies, headers, or search params, or call fetch without a cache policy. Three controls let you pin or override that inference:
- Route segment config —
export const dynamic = "force-static"or"force-dynamic", plusexport const revalidate = 60for ISR at the segment level. - Per-fetch cache options —
next: { revalidate, tags, cache }control one data dependency without affecting the whole route. Tags enable on-demand invalidation withrevalidateTag. - Suspense and PPR — wrap dynamic regions in
<Suspense>so the static parts of a route prerender while dynamic holes stream, which is how a single page blends SSG and SSR.
In the Pages Router the mapping is more explicit and more coarse: getStaticProps plus revalidate is ISR, adding getStaticPaths is SSG, and getServerSideProps is SSR. The App Router's fetch-level granularity is what makes per-route and even per-component mixing practical.
Common Mistakes
- SSR by default where ISR would do — rendering a catalog page on every request because one field is dynamic. Split the dynamic field into a Suspense hole and keep the shell static.
- Caching authenticated responses — a shared CDN cache holding one user's HTML for the next visitor. Anything that reads
cookies()must be dynamic and private. - No staleness bound stated — teams pick ISR with
revalidate: 3600without writing down whether an hour of stale pricing is acceptable. Decide the number from the business, not the default. - CSR for public landing pages — the fastest route to missing rich results, weak social previews, and slow LCP on mobile.
- Full rebuilds instead of on-demand revalidation — a 40-minute static site rebuild to publish one article is an ISR configuration problem, not an infrastructure problem.
Conclusion
There is no single winning rendering strategy, because there is no single answer to "how often does this data change and who is allowed to see it?" SSG gives you the best latency and the weakest freshness; SSR gives you perfect freshness at the highest cost; CSR moves all the work to the client and trades SEO for interactivity; ISR sits in the middle as the pragmatic default for most public content.
Modern frameworks have made these choices per route and even per component. Treat rendering strategy as a data-architecture decision you make deliberately for each section of your application, document the staleness bound you chose, and reach for the most static option that still keeps the content correct.
Key Takeaways
- CSR renders in the browser, SSG at build time, SSR per request, ISR per request with background regeneration — chosen by how often data changes.
- SSG/ISR win LCP and SEO by returning full HTML from the CDN; CSR costs a second network waterfall before meaningful paint.
- ISR is stale-while-revalidate at the page level: users get the cached copy instantly while the server rebuilds in the background.
- Modern Next.js derives rendering dynamically from your code and lets you pin it with force-static / force-dynamic, per-fetch revalidate, and tags.
- PPR merges static and dynamic in one route: a prerendered shell with Suspense holes that stream fresh data at request time.
FAQ
What is the difference between SSR and SSG?
SSG (static site generation) renders HTML once at build time and serves the same file to every visitor from a CDN, so it is extremely fast but only as fresh as your last deploy. SSR (server-side rendering) renders HTML on every request on the server, so the content is always current but each visitor waits for your server and database. ISR is the middle ground: static files with a revalidate window that refreshes them in the background.
Is ISR better than SSR?
Not universally. ISR is better whenever content can tolerate minutes of staleness — product pages, docs, pricing — because it delivers CDN-speed responses with near-real-time freshness and almost no origin load. SSR is better when the response must be current per request or per user: carts, dashboards, search results, and anything reading cookies. Choose based on your staleness budget, not on which is newer.
Which rendering strategy is best for SEO?
SSG and ISR are best for SEO: crawlers receive complete HTML with metadata and structured data on the first response, with no JavaScript execution required. SSR is equally indexable and is required when content must be current at crawl time. CSR can be indexed by Googlebot but is less reliable across other crawlers and AI answer engines, so public landing pages should always be server-rendered.
How do I choose a rendering strategy in Next.js App Router?
Next.js infers it: a route is static unless your code reads cookies, headers, or search params, or performs an uncached fetch. You can pin the behavior with export const dynamic = "force-static" or "force-dynamic", set ISR with export const revalidate = 60, or control caching per fetch with next: { revalidate, tags }. Wrap slow dynamic regions in Suspense so the rest of the page stays static.
Does client-side rendering hurt Core Web Vitals?
It usually hurts LCP, because the largest element cannot paint until the JavaScript bundle downloads, executes, and fetches data over a second network round trip. Its effect on INP depends on bundle size and hydration cost rather than on CSR itself, and it can raise CLS when dynamic content replaces skeletons with different dimensions. Server-rendered HTML with stable layouts gives you the most consistent field data.
References
- Next.js. "Rendering." Next.js Docs. nextjs.org/docs/app/building-your-application/rendering
- Next.js. "Time-Based Revalidation (ISR)." Next.js Docs. nextjs.org/docs/app/building-your-application/data-fetching/revalidating
- web.dev. "Rendering on the Web." Google web.dev. web.dev/articles/rendering-on-the-web
- web.dev. "Largest Contentful Paint (LCP)." web.dev. web.dev/articles/lcp
- web.dev. "Cumulative Layout Shift (CLS)." web.dev. web.dev/articles/cls
- Google Search Central. "Crawling and indexing JavaScript." developers.google.com/search/docs/crawling-indexing/javascript