CSR · SSR · SSG · ISR · Streaming SSR · RSC. One question — where and when does HTML get built? — and a spectrum of answers, each trading TTFB, FCP, interactivity, freshness, and SEO. The Lead skill is picking the right point per surface and defending it.
Don't memorize six acronyms as a list. They're all answers to one question: where (browser / server / build machine) and when (build-time / request-time / on-demand) do we turn components + data into the HTML the user first sees — and how much JavaScript do we then ship to make it interactive?
Two responses sit at the extremes, everything else is in between: [web.dev — Rendering on the Web]
<div id="root"> + a JS bundle. The browser builds the HTML at request time. Fast server, slow first paint, bad SEO without help.Between them: SSR (build HTML per request on the server), ISR (static, but regenerate in the background), streaming SSR (send HTML in chunks as it's ready), and RSC (render some components on the server and never ship their JS). The metrics they trade:
| Metric | Means | Helped by |
|---|---|---|
| TTFB | time to first byte — how fast the server starts replying | static/CDN (fast); per-request SSR (slower) |
| FCP / LCP | first/largest content painted | server-rendered HTML (content in the first response) |
| TTI / INP | when it's actually interactive / input latency | shipping less JS to hydrate (RSC, islands) |
| SEO | crawlers see real content, not a spinner | any server/static HTML |
| Freshness | how current the data is | SSR (per request); ISR (windowed); SSG (build only) |
“These aren't six choices — it's one axis: where and when the HTML is built. I pick the point per surface that balances TTFB, first paint, interactivity, and freshness.”
Empty shell + JS; browser fetches data & renders. Fast TTFB, slow FCP/LCP (blank until JS runs), poor SEO. Great for dashboards / logged-in tools behind auth. The classic SPA.
Server builds full HTML per request. Fast FCP, SEO-ready, fresh — but TTFB grows with server work, and you still ship JS to hydrate. The default for a logged-out search/listing page.
HTML built once at build time, served from CDN. Best TTFB/FCP, trivially cacheable, cheap. The catch: stale until you rebuild, and slow builds at scale. Marketing, docs, blog, landing pages.
SSG + a revalidate window: serve static instantly, regenerate in the background on a timer or on-demand. Static speed with eventual freshness — SWR for whole pages. Millions of hotel/city pages without rebuilding all of them.
Server flushes HTML in chunks as it's ready (via <Suspense>): shell + above-the-fold instantly, slow widgets stream in. TTFB no longer hostage to the slowest query. §3.
Components that render only on the server and ship zero JS to the client; interactive bits opt in with "use client". Less hydration → better INP. §4.
Critically, these are not mutually exclusive. A modern app mixes them per route and per component: a marketing page is SSG, the search results are streaming-SSR, the filter panel is a client island, the hotel detail page is ISR. Picking one strategy for the whole app is a junior tell. [patterns.dev]
“There's no single ‘right’ strategy — I choose per surface. Static for content, SSR/streaming for the SEO listing page, a client island for the interactive filters.”
Server rendering has a hidden cost. The server sends HTML, the user sees it fast — but it isn't interactive yet. The browser must download the JS, then hydrate: re-run the component tree on the client to attach event handlers and rebuild state, matching the server HTML. Until hydration finishes, buttons look ready but do nothing — the “uncanny valley.” Hydration roughly re-does the server's work on a weaker device, and it's a top source of poor INP. [web.dev] (Lesson 10 goes deep on hydration; here it's the motivation.)
Classic SSR is also all-or-nothing in two ways: the server can't send any HTML until every data fetch resolves (TTFB held hostage by the slowest query), and the client can't make any part interactive until the whole bundle hydrates. Streaming SSR + <Suspense> breaks both:
<Layout>
<SearchResults /> // fast — render & flush immediately
<Suspense fallback={<Skeleton/>}>
<Reviews /> // slow query — stream in when ready
</Suspense>
</Layout>
The server flushes the shell + results now with a skeleton in the Reviews slot, keeps the connection open, and pushes the Reviews HTML when its query resolves. React also does selective hydration: it hydrates the streamed-in chunks independently, and prioritises whatever the user interacts with first. Fast first paint without letting one slow widget block the page. [react.dev — Suspense]
200 and start streaming before slow data exists, so a late failure can't become a clean HTTP error — you handle it with an error boundary that streams a fallback instead. SEO is fine (Google parses the streamed document), but you must verify crawlers get the streamed content. And a fast-but-empty shell can hurt LCP/CLS if the real LCP element streams in late and shifts layout — reserve space for it.
“Classic SSR is hostage to its slowest query and hydrates all-or-nothing. Streaming with Suspense flushes the shell now and streams the slow parts in — first paint stops waiting on Reviews.”
Streaming fixes when HTML arrives; React Server Components attack the deeper cost — how much JS you ship at all. An RSC renders only on the server and sends its output to the client as a serialized description — but ships zero JavaScript for itself. Data-fetching, markdown rendering, big formatting libs, DB access all live server-side and never enter the bundle. Components that need interactivity (state, effects, event handlers) opt back in with the "use client" directive, marking the server/client boundary. [react.dev — Server Components]
// Server Component (default) — runs on server, 0 KB to client
async function HotelPage({id}) {
const hotel = await db.hotels.find(id); // DB access, no API layer
return <article>{hotel.name} <BookButton id={id} /></article>;
}
// BookButton.tsx
"use client" // ← this island ships JS & hydrates; the rest doesn't
The payoff is less hydration → better TTI/INP, because most of the tree was never interactive to begin with. RSC is the architectural answer to the hydration tax in §3: don't make hydration faster — have less to hydrate.
“Streaming makes hydration arrive sooner; RSC makes there be less of it. Server Components ship zero JS, so only the interactive islands hydrate — the cheapest hydration is the one you never do.”
Everything above picks a strategy per route. But almost every real page is mixed: a hotel page is ~90% static (layout, description, photos, map) and ~10% dynamic-and-personal (live price, your loyalty discount, “3 people viewing”). The old choice forced a bad trade — render the whole page dynamic (lose the edge-cached static TTFB) or render it static and fetch the dynamic bits client-side (a request waterfall + a flash of empty price). PPR collapses the static-vs-dynamic decision from per-route to per-component, in one page and one HTTP response. [Next.js — PPR]
<Suspense> boundary.<Suspense>, reading request data like cookies()/headers()/searchParams) are left as holes, with the fallback baked in.export default function HotelPage() {
return (<>
<HotelHeader /> // static → prerendered shell, edge-cached
<Gallery /> // static
<Suspense fallback={<PriceSkeleton/>}>
<LivePrice /> // dynamic hole → streamed per request
</Suspense>
</>);
}
The model that makes it click: it's islands inverted. Lesson 10's islands are interactive islands in a static client page; PPR is dynamic holes in a static server page. The same <Suspense> boundary you know from streaming SSR (§3) now does double duty as the marker that separates static from dynamic. One response, static's TTFB and dynamic's freshness, no client waterfall — the clean version of the “ISR + streaming” hotel page in §6.
“PPR collapses the static-vs-dynamic call from per-route to per-component — a static shell served instantly from the edge, with dynamic holes streamed into the same response. It's islands inverted: dynamic holes in a static page.”
This is the deep-dive the panel flagged. Don't answer “use SSR.” Walk the surfaces of a hotel search/booking flow and assign each a strategy with a reason: [web.dev]
| Surface | Strategy | Why |
|---|---|---|
| Marketing / city landing / SEO content | SSG / ISR | SEO-critical, mostly static, huge traffic → static at the edge; ISR to refresh prices/inventory without full rebuilds. |
| Search results listing | Streaming SSR | SEO + must paint fast on real content; results stream while slow facets/maps/reviews fill in. Edge-cache the logged-out variant. |
| Filters / date picker / map interactions | Client island (CSR) | Highly interactive, user-specific, no SEO value → hydrate just this; with RSC it's a small "use client" island. |
| Hotel detail page | ISR + streaming | Millions of pages, mostly stable content → static with revalidation; stream the live price/availability widget. |
| Checkout / account | SSR (no/short cache), CSR-heavy | Personalized, correctness-critical, behind auth → no SEO need, fresh authoritative data, no-store (Lesson 08's checkout rule). |
Then layer the cross-cutting concerns that make it a Lead answer: edge caching the logged-out HTML (Lesson 08), code-splitting per route (Lesson 05), i18n — locale routing + per-locale static/edge variants with hreflang, and a RUM guardrail on p75 LCP/INP across markets.
Concept: rendering is one axis (where/when HTML is built); I assign each surface a point on it. Trade-off: SSR buys SEO + fresh first paint but costs TTFB and a hydration tax; static buys speed but staleness — so I use ISR for the middle and streaming/RSC to pay down hydration. Anchor: “We made the SEO listing streaming-SSR behind edge cache, the detail pages ISR, the filters a client island, and checkout fresh-SSR with no-store — FCP and INP both improved while SEO held.” Impact: first paint + crawlability drive organic traffic and conversion; less JS shipped drives INP across low-end devices in emerging markets. Invite: “If SEO weren't a factor — say a logged-in dashboard — I'd happily go CSR and skip the SSR complexity entirely.”
Pick an answer; instant feedback. Push-back style, like the round.
1. Strip the buzzwords: what single question do CSR/SSR/SSG/ISR/RSC all answer?
2. A logged-in internal analytics dashboard, no SEO need, behind auth. Best default?
3. What's the core difference between SSG and SSR?
4. the platform has millions of hotel pages, mostly stable, but prices change. Full SSG rebuilds take too long; SSR per request is wasteful. What fits?
5. What is the "hydration tax," and why does it hurt INP?
6. The results page is fast but the reviews query is slow. In classic (non-streaming) SSR, what happens to TTFB?
7. What does wrapping the slow <Reviews/> in <Suspense fallback={...}> during streaming SSR achieve?
8. How do React Server Components reduce shipped JavaScript?
9. A prop you pass from a Server Component to a Client Component must be…
scn: you try to pass an onClick handler / a class instance across the boundary.
10. Whiteboard: the SEO-critical, logged-out search results page for a hotel site. Strongest single choice?
11. Interviewer pushes: "So you'd SSR the whole app?" Best Lead response?
12. A hotel page is mostly static (layout, photos, description) with a live price + personalized discount. You want the static part edge-cached for TTFB and the price fresh per request, in one response. What fits?
0 / 12 answered