Concept
The beginner framing: Next.js can render your pages in several different ways, ahead of time, on every request, in the browser, or in pieces as they become ready, and picking the right one per route is a major part of building a fast app.
The precise mental model: these terms collapse onto two independent questions. When does rendering happen, at build time (or on a scheduled revalidation), at request time on the server, or in the browser after the page loads? And does the response arrive all at once, or progressively? The App Router doesn't ask you to pick a strategy up front the way the Pages Router's named functions did (see Pages Router), it derives the strategy from what your Server Components actually do.
| Term | When it renders | App Router trigger |
|---|---|---|
| SSG (Static Site Generation) | Build time, once | A route with no runtime API access, everything can be prerendered |
| ISR (Incremental Static Regeneration) | Build time, then periodically refreshed | "use cache" + cacheLife, or the legacy revalidate, SSG plus a revalidation window |
| SSR (Server-Side Rendering) | Every request, on the server | A route reads cookies(), headers(), or searchParams, this is what "Dynamic Rendering" means |
| CSR (Client-Side Rendering) | In the browser, after JS loads | A Client Component fetching its own data (via use(), SWR, React Query) |
| Streaming | Not a timing, a delivery mechanism | <Suspense>/loading.tsx sending the shell immediately, dynamic parts arriving later |
Streaming is the answer to "but SSR is slow"
<Layout><Header /><Suspense fallback={<Skeleton />}><SlowWidget /> {/* data not ready on the server yet */}</Suspense></Layout>
The server doesn't wait for SlowWidget's data before sending anything, it streams the shell HTML plus the fallback's HTML right away, so the browser can paint immediately.
Streaming doesn't change when a dynamic route's data is computed, it changes how the response is delivered. Instead of blocking the entire response on the slowest piece of data, the static shell (everything cacheable) is sent immediately, and the genuinely dynamic parts, wrapped in <Suspense>, stream in as follow-up chunks. This is the core of the "static shell + dynamic holes" model covered in depth in Caching.
CSR didn't go away, it's just opt-in now
"use client";
import { useEffect, useState } from "react";
export function LiveTicker() {
const [price, setPrice] = useState(null);
useEffect(() => {
const id = setInterval(() => fetchPrice().then(setPrice), 1000);
return () => clearInterval(id);
}, []);
return <span>{price}</span>;
}Anything genuinely client-only and frequently changing, a live price ticker, a chat window, a canvas-based editor, is exactly what Client Component data fetching is for. App Router didn't remove CSR; it just made Server Components (and everything above this table) the default, so CSR is a deliberate choice for the pieces that actually need it, rather than the starting point for everything.
Try It
Predict the rendering classification before checking the solution.
export default async function Page() {
const cookieStore = await cookies();
const theme = cookieStore.get("theme")?.value ?? "light";
return <div className={theme}>Hello</div>;
}Is this SSG, ISR, SSR, or CSR?
Solution
SSR (dynamic rendering). Reading cookies() is a runtime API access, the value genuinely isn't known until an actual request arrives, so Next.js cannot prerender this page at build time. Without any "use cache" directive wrapping the runtime-dependent part, the whole component (and typically the route) is rendered fresh on the server for every request.
Implement It Yourself
Build a minimal classifier that mirrors the actual decision Next.js makes:
function classifyRendering({ usesRuntimeAPI, hasUseCache, isClientComponent }) {
if (isClientComponent) {
return "CSR, renders in the browser after hydration";
}
if (usesRuntimeAPI && !hasUseCache) {
return "SSR (Dynamic Rendering), computed fresh on every request";
}
if (hasUseCache) {
return "ISR, prerendered, refreshed per cacheLife's revalidate window";
}
return "SSG, fully prerendered at build time, no runtime dependency at all";
}
classifyRendering({ usesRuntimeAPI: true, hasUseCache: false, isClientComponent: false });
// "SSR (Dynamic Rendering), computed fresh on every request"
classifyRendering({ usesRuntimeAPI: false
This mirrors the actual decision tree: the strategy isn't declared up front, it falls out of what the component actually touches (a runtime API, a caching directive, or the client boundary).
Under the Hood
The hydration mechanics that make server-rendered HTML interactive, whether that HTML was generated at build time (SSG/ISR) or per-request (SSR) makes no difference to this step, are covered in full in SSR, Hydration & Streaming: hydration walks and matches the existing DOM regardless of when it was produced. And streaming's ability to send a shell immediately and fill in dynamic holes afterward is the exact Suspense mechanism from Suspense, just applied at the whole-route level via loading.tsx instead of a single component.
Common Mistakes
1. Treating "Server Component" and "SSR" as synonyms
A Server Component can be either statically prerendered (SSG/ISR) or dynamically server-rendered (SSR), which one depends entirely on whether it touches runtime data, not on the fact that it's a Server Component at all. Every route in the App Router uses Server Components by default; only some of those routes are actually "SSR" in the classic sense.
2. Assuming reading searchParams anywhere only affects that one component
export default async function Page({ searchParams }: PageProps<"/search">) {
const { q } = await searchParams; // this makes the WHOLE route dynamic, not just this line
return <Results query={q} />;
}Accessing a runtime API opts the entire route into dynamic rendering by default, the fix, if only part of the page genuinely needs it, is isolating that access behind its own <Suspense> boundary (see Caching's static-shell model).
3. Assuming CSR is deprecated or discouraged entirely
CSR remains completely valid, it's simply opt-in via "use client" now, reserved for genuinely client-only, highly interactive, or frequently-updating UI, rather than being the default rendering path for the whole app.
Best Practices
- Default to static/cached rendering (SSG/ISR) for anything that doesn't need to be personalized or live per request, it's the fastest and cheapest option by a wide margin.
- Reserve dynamic rendering (SSR) for the specific parts that genuinely need request-time freshness (a user's session-specific greeting, live search results), and isolate that access behind Suspense if the rest of the page doesn't need it.
- Use CSR deliberately for state that's inherently client-only or updates too frequently to be worth server round-trips.
- Lean on streaming for any dynamic route with a slow dependency, it's almost always better than blocking the entire response.
Performance Tips
- SSG/ISR content can be served directly from a CDN with zero per-request server compute, the fastest possible response for content that doesn't need personalization.
- Streaming decouples "time to first byte" from "time until the slowest data resolves", a dynamic route with one slow dependency no longer has to make the whole page wait on it.
- CSR shifts render work to the client's device, appropriate for already-authenticated, highly interactive views where SEO doesn't matter, but a poor choice for anything that needs to be fast on first load or indexable by search engines.
