Rendering on the Web - The Three Knobs Behind Every Paradigm
Written on: May, 2026
•7 min readCSR, SSR, SSG, ISR, RSC. Release posts treat each one like a new religion. They are not. They are coordinates on the same three knobs: where HTML is built, when it is built, and what crosses the wire. Learn the knobs and the acronyms stop scaring you.
Steps
- The three knobs (and how we got here in one lap)
- CSR: the browser owns the page
- SSR with hydration: fast paint, delayed hands
- RSC: the third knob actually moves
- Knob sheet and a blunt way to choose
Knob one: where does the UI get built (server, build machine, or browser)? Knob two: when (every request, once at build, first hit then cache, or different answers per chunk)? Knob three: what actually crosses the wire (plain HTML, HTML plus a fat JS bundle, or a serialized component tree with client islands)? Every named paradigm is just a point in that space. The industry did not invent seven unrelated ideas; it kept moving those coordinates when networks, crawlers, and product expectations moved. Classic server templates answered per request with HTML. Static generators moved the work to build time so a CDN could serve cold files. SPAs pushed rendering into the browser and shipped an empty shell plus a bundle. SSR plus hydration brought HTML back for first paint but still paid the SPA tax twice. ISR and friends cached HTML so most hits stayed CDN-cheap. Streaming let fast chunks leave the server while slow ones caught up. Server Components finally changed the payload shape so not every server-rendered subtree had to arrive as executable JS in the client. The table below lists the coordinates explicitly; the next sections zoom in on three moments where the tradeoffs hurt enough that teams changed how they shipped.
Move all rendering to the client and the first response is honest: almost no HTML, almost all responsibility deferred to a bundle. Interactivity and client-side routing feel great once the app is warm. The cost is the gap between first byte and usable UI (download, parse, execute), plus SEO and low-end devices paying again on every navigation. Coordinate: browser, runtime in the browser, empty shell plus JS bundle.
1<!-- Typical CSR first response: the page is a promise, not a document -->
2<!DOCTYPE html>
3<html>
4 <head><title>My App</title></head>
5 <body>
6 <div id="root"></div>
7 <script src="/bundle.js"></script>
8 </body>
9</html>Ship HTML from the server so something renders immediately, then send the same component graph again as JavaScript so clicks and state work. Users see layout early; they do not get a trustworthy interactive surface until hydration finishes. You still ship the SPA-sized bundle; you added server render cost on top. People call it shipping the page twice because that is what the runtime does. Coordinate: server then browser, usually every request unless you layer caching, HTML plus bundle.
1// Hydration on a mid-range phone (illustrative, not a benchmark)
2t = 0ms → request leaves the browser
3t = 20ms → HTML arrives, paint happens
4t = 400ms → bundle download completes
5t = 600ms → hydration completes; buttons start working
6
7// Between paint and hydration: visible, not reliably interactive.1GET /dashboard
2 ↳ <html>...server-rendered markup...</html>
3 ↳ <script src="/bundle.js"></script> // same tree, second passFor a long time the payload stayed “HTML, maybe with a bundle beside it.” Server Components change what crosses the wire: a serialized tree where server-run subtrees contribute rendered output without shipping their component implementation to the client, and client components show up as islands that still hydrate from the bundle. The upside is less JS for read-mostly UI; the downside is a stricter split between server and client components and more design work at the boundary. Coordinate: server plus browser, often per chunk or flight, serialized tree plus client islands. Real wire formats differ by framework; the JSON sketch is intentionally schematic.
1// Schematic RSC-style payload (illustrative, not a literal network capture)
2{
3 "tree": [
4 {
5 "type": "div",
6 "props": { "class": "page" },
7 "children": [
8 {
9 "kind": "rendered-output",
10 "html": "<h1>Welcome back, Aldi</h1>"
11 },
12 {
13 "kind": "client-component",
14 "name": "InteractiveCart",
15 "props": { "items": [{ "id": 1, "name": "Mochi" }] }
16 },
17 {
18 "kind": "rendered-output",
19 "html": "<footer>© 2026</footer>"
20 }
21 ]
22 }
23 ]
24}
25
26// Outer sections ship as output; the cart is the island that still pulls code.Use the grid as the source of truth. When someone asks “should we use X,” translate X into where/when/what, then check whether that matches data freshness, personalization, and how much client JS you can justify. Heuristics without nuance: if it can be fixed at build time, prefer SSG; if it changes on a schedule or after publish events, cache rendered HTML (ISR-shaped); if it is per-user or must be fresh every request, render on the server; if one slow fragment blocks the rest, stream; if you need dense server UI with small interactive pockets, look at RSC-style splits. Marketing names change; the knobs do not.
1Paradigm | Where | When | What crosses the wire
2---------------------------+------------------------+----------------------------+-----------------------------------------
3Classic server-rendered | server | every request | HTML
4SSG | build machine | once at build | HTML
5SPA / CSR | browser | in-browser at runtime | empty shell + JS bundle
6SSR + hydration | server, then browser | every request | HTML + JS bundle
7ISR | server | first request, then cached | HTML (cached)
8Streaming SSR | server | per chunk | HTML, streamed
9RSC | server + browser | per chunk | serialized component tree + client islands