Rendering strategies (CSR/SSR/SSG/ISR)
CSR, SSR, SSG: where and when the HTML is built
Every strategy is a point on two axes: WHERE the HTML is built (browser, server, or build machine) and WHEN (per request, at build, or periodically). Hydration is a separate step that re-attaches interactivity wherever the HTML came from.
| Strategy | HTML built | TTFB | First paint | SEO | Server cost |
|---|---|---|---|---|---|
| CSR | in the browser, from JS | fast | slow (blank until JS runs) | weak without prerender | low |
| SSR | on the server, per request | slower | fast (content in the first bytes) | strong | high |
| SSG | at build, served from a CDN | fast | fast | strong | near zero at runtime |
SSG renders once at build and freezes that HTML until the next build, so it is wrong for per-request or personalized data.
Hydration attaches JS to server HTML
Server-rendered HTML is not interactive on arrival. Hydration runs the same components in the browser and wires event listeners and state onto the existing DOM without rebuilding it.
// CSR: build the DOM from scratch in the browser
createRoot(document.getElementById("root")).render(<App />);
// SSR: attach listeners to HTML the server already sent
hydrateRoot(document.getElementById("root"), <App />);
Between first paint and the end of hydration the page looks ready but drops clicks, the gap web.dev calls the uncanny valley.
Wrong "SSR sends HTML instead of JS, so it shrinks the bundle." The server sends the HTML and a full copy of the component code, which then hydrates. Right SSR and SSG buy faster first paint and SEO, not a smaller bundle.
ISR serves stale while it revalidates
Incremental static regeneration keeps static speed but refreshes pages on a schedule or on demand, with no full rebuild. In Next.js you set export const revalidate = 3600 on the route. Past the window, the next request still gets the stale page instantly, the cache is marked invalid, a fresh copy builds in the background, and later requests get the new one. Freshness is eventual, so the first visitor after expiry sees old data. The x-nextjs-cache header reads HIT, STALE, or MISS so you can watch it happen.
Choosing by freshness, personalization, SEO
Decide with three questions. How often does the content change: never between deploys points to SSG, periodic to ISR, live to SSR. Is it personalized per request via cookies, auth, or geo? If yes, SSG is out, because one HTML serves everyone. Does SEO or first paint on slow devices matter? That favors SSR or SSG over CSR. Frameworks apply this per route, not once for the whole app.
Only islands and Server Components cut shipped JS
SSR and SSG do not reduce the JavaScript the browser downloads; they add an HTML payload on top of the same bundle, the data-duplication cost web.dev describes. Two techniques cut it.
Islands architecture (Astro) renders most of the page as static HTML with zero JS and hydrates only the interactive islands, each independently. Directives control timing: client:load, client:idle, client:visible (hydrate when scrolled into view), and client:only (skip the server render entirely, so that piece behaves like CSR with no SEO or first-paint HTML).
React Server Components run on the server and send their rendered output, not their component code, so heavy dependencies used to build the markup never reach the client. The catch: a Server Component cannot hold state or handlers. Anything interactive stays a Client Component, which still ships and hydrates its JS.
Wrong "React Server Components and SSR are the same lever for less JS." SSR still hydrates a Client Component's full code; only the server-only parts of the tree stay off the client.
Streaming SSR and selective hydration
Rather than build the whole page with renderToString and send it at once, streaming SSR sends HTML in chunks. renderToPipeableStream (Node) and renderToReadableStream (Web and edge runtimes) flush a shell immediately and stream slow subtrees wrapped in <Suspense> as their data resolves, which improves first paint. React then hydrates those Suspense boundaries selectively and prioritizes the parts the user interacts with first, so one slow section no longer blocks the whole page from becoming interactive.
Hydration mismatches come from server and client disagreeing
Hydration assumes the client render matches the server HTML. Rendering Date.now(), Math.random(), a locale or timezone value, or a typeof window branch during render produces different output on each side and throws a mismatch error.
// The first client render must match the server, so gate client-only UI:
const [ready, setReady] = useState(false);
useEffect(() => setReady(true), []); // runs in the browser, after hydration
return ready ? <LocalTime /> : null;
For a single node you expect to differ, like a timestamp, set suppressHydrationWarning on it instead.
Next.js 15 flipped its caching defaults
The trap on current Next: fetch requests, GET Route Handlers, and client navigations are no longer cached by default. In Next 14 fetch defaulted to force-cache; in Next 15 it is uncached, and you opt back in with fetch(url, { cache: 'force-cache' }), { next: { revalidate: N } }, or export const dynamic = 'force-static'. A no-store or revalidate: 0 fetch flips the whole route to dynamic, and when one route has fetches with different windows the lowest revalidate wins. Partial Prerendering, which streams dynamic holes inside a static shell, is experimental in Next 15, not the default.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.