---
title: "23 — SSR — When the Server Paints the First Frame"
uid: server-side-rendering
tags: ["seo", "react", "vue", "ssr", "nuxt", "rendering", "roadmap:frontend", "nextjs"]
excerpt: "Rendering is a spectrum of when and where the HTML is generated — not a single technique. SSR is one point: the server builds the HTML on each request."
date: 2026-08-12T18:35:08+0000
source: https://www.aveshina.my.id/en/blog/server-side-rendering
---

"A Next.js feature" was how I filed server-side rendering, which conflated the technique with the frameworks that sell it. The separation: **rendering is a spectrum of _when_ and _where_ the HTML is generated, not a single technique.** SSR is one point on that spectrum — the point where the server builds the HTML on each request, before the browser sees the page.

"Server-side rendering" gets used two ways: narrowly, a specific mode (per-request, on the server); broadly, the whole family of "let the server produce HTML instead of leaving it to the browser." Only the spectrum view makes the trade-offs legible, so that's where I start.

## The spectrum: when and where HTML is born

Every page has to become HTML at _some_ point — the question is when, and on which machine. Four answers, and most production sites mix them [1][2]:

- **CSR (Client-Side Rendering).** The server ships a near-empty HTML shell plus a JS bundle. The browser downloads it, runs it, and the UI gets built in the browser. First paint is late and depends on JS executing.
- **SSR (Server-Side Rendering).** The server runs the component tree on _every request_ and returns finished HTML. The browser paints real content immediately; JS then "hydrates" it — attaches the interactive behavior to the already-rendered HTML — to make it interactive [3].
- **SSG (Static Site Generation).** HTML is rendered _once_, at build time. The server just hands out prebuilt files. Fast and cheap, but frozen until the next build.
- **ISR (Incremental Static Regeneration).** Build-time HTML the server revalidates on a schedule or on demand. Static speed with the freshness of SSR.

Same page, four different moments the HTML exists:

```figure
<svg viewBox="0 0 740 320" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="A horizontal timeline comparing when HTML first exists under four rendering modes. Four rows: CSR, SSR, SSG, ISR. A left arrow label says build time, a right arrow label says user request, and a far-right marker says first paint. CSR: HTML is empty at request, real HTML appears in the browser just before first paint, marked late. SSR: real HTML is generated on the server at request time and arrives ready, first paint is early. SSG: real HTML exists since build time, served instantly, first paint earliest. ISR: real HTML from build time, revalidated around request, first paint early.">
  <defs>
    <marker id="ssarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>

  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- time axis -->
    <line x1="40" y1="270" x2="700" y2="270" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#ssarrow)"/>
    <text x="80" y="290" font-size="10.5" font-weight="700" fill="#64748b" text-anchor="middle">build time</text>
    <text x="380" y="290" font-size="10.5" font-weight="700" fill="#64748b" text-anchor="middle">user request</text>
    <text x="660" y="290" font-size="10.5" font-weight="700" fill="#64748b" text-anchor="middle">first paint</text>
    <line x1="80" y1="264" x2="80" y2="276" stroke="#94a3b8" stroke-width="1.5"/>
    <line x1="380" y1="264" x2="380" y2="276" stroke="#94a3b8" stroke-width="1.5"/>
    <line x1="660" y1="264" x2="660" y2="276" stroke="#94a3b8" stroke-width="1.5"/>

    <!-- CSR row -->
    <text x="30" y="48" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="end" dominant-baseline="middle">CSR</text>
    <rect x="40" y="38" width="640" height="20" rx="4" fill="#e2e8f0"/>
    <rect x="380" y="38" width="120" height="20" rx="4" fill="#fca5a5" stroke="#dc2626" stroke-width="1.2"/>
    <text x="440" y="49" font-size="9.5" font-weight="700" fill="#7f1d1d" text-anchor="middle">empty shell + JS</text>
    <rect x="560" y="38" width="100" height="20" rx="4" fill="#fecaca" stroke="#dc2626" stroke-width="1.2"/>
    <text x="610" y="49" font-size="9.5" font-weight="700" fill="#7f1d1d" text-anchor="middle">HTML built late</text>
    <text x="690" y="49" font-size="10" font-weight="700" fill="#dc2626" text-anchor="end">late</text>

    <!-- SSR row -->
    <text x="30" y="110" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="end" dominant-baseline="middle">SSR</text>
    <rect x="40" y="100" width="640" height="20" rx="4" fill="#e2e8f0"/>
    <rect x="380" y="100" width="120" height="20" rx="4" fill="#bfdbfe" stroke="#2563eb" stroke-width="1.2"/>
    <text x="440" y="111" font-size="9.5" font-weight="700" fill="#1e3a8a" text-anchor="middle">server renders</text>
    <rect x="500" y="100" width="100" height="20" rx="4" fill="#93c5fd" stroke="#2563eb" stroke-width="1.2"/>
    <text x="550" y="111" font-size="9.5" font-weight="700" fill="#1e3a8a" text-anchor="middle">HTML ready</text>
    <text x="690" y="111" font-size="10" font-weight="700" fill="#2563eb" text-anchor="end">early</text>

    <!-- SSG row -->
    <text x="30" y="172" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="end" dominant-baseline="middle">SSG</text>
    <rect x="40" y="162" width="640" height="20" rx="4" fill="#e2e8f0"/>
    <rect x="80" y="162" width="200" height="20" rx="4" fill="#bbf7d0" stroke="#16a34a" stroke-width="1.2"/>
    <text x="180" y="173" font-size="9.5" font-weight="700" fill="#14532d" text-anchor="middle">built once · frozen</text>
    <rect x="380" y="162" width="120" height="20" rx="4" fill="#86efac" stroke="#16a34a" stroke-width="1.2"/>
    <text x="440" y="173" font-size="9.5" font-weight="700" fill="#14532d" text-anchor="middle">served instantly</text>
    <text x="690" y="173" font-size="10" font-weight="700" fill="#16a34a" text-anchor="end">earliest</text>

    <!-- ISR row -->
    <text x="30" y="234" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="end" dominant-baseline="middle">ISR</text>
    <rect x="40" y="224" width="640" height="20" rx="4" fill="#e2e8f0"/>
    <rect x="80" y="224" width="200" height="20" rx="4" fill="#bbf7d0" stroke="#16a34a" stroke-width="1.2"/>
    <text x="180" y="235" font-size="9.5" font-weight="700" fill="#14532d" text-anchor="middle">built · then revalidated</text>
    <rect x="380" y="224" width="120" height="20" rx="4" fill="#fde68a" stroke="#ca8a04" stroke-width="1.2"/>
    <text x="440" y="235" font-size="9.5" font-weight="700" fill="#422006" text-anchor="middle">stale-while-revalidate</text>
    <text x="690" y="235" font-size="10" font-weight="700" fill="#ca8a04" text-anchor="end">early</text>

    <text x="370" y="312" font-size="11" fill="#64748b" text-anchor="middle" font-style="italic">same page — four different moments the HTML becomes real</text>
  </g>
</svg>
```

The shortcut I use now: **CSR pushes the work onto the user's device and network; SSR (and its siblings) pull it back onto my server.** The SEO, first-paint, and cost arguments are all consequences of that one shift.

## Why SSR exists: content sooner, content crawlable

Two pressures pushed rendering back onto the server, and they're not the same problem.

**First paint.** Under pure CSR the browser receives an empty <div id="root"></div> and a script tag. The user stares at a blank screen until the JS downloads, parses, executes, fetches data, and writes to the DOM. On a slow phone that's a long, visibly empty wait. SSR shortens it: the HTML that comes back already _is_ the page, so the browser paints content on the first frame and hydrates it into an app afterward [3]. Interactivity still needs JS; the _content_ doesn't wait for it.

**Crawlability.** A search crawler historically had limited patience for running JavaScript [4]. Send it an empty shell and it may index exactly that — nothing. Send it rendered HTML and it reads headings, links, and text like a browser. That's why SSR is non-negotiable for content sites that depend on search traffic: the page has to _be_ something before any script runs.

So SSR trades one kind of cost for another. I give up the simplicity of a static bundle and the cheapness of a CDN-only deploy, and in return I take on a running server that has to render on every request. For a marketing site or a docs site, that trade is often a bad one and SSG wins. For a dashboard, an e-commerce catalog, or anything per-user and time-sensitive, SSR is what makes the page actually useful.

## The trade-off, said plainly

SSR is not free, and the version I had in my head — "just turn it on, it's better" — was what needed correcting.

- **Server load.** Every request executes my component tree, often with data fetches. Real CPU on a real box, scaled with traffic. A viral spike under CSR mostly hits my CDN; under SSR it hits my render servers.
- **Latency to first byte.** The browser can't paint until the server finishes rendering. A slow database query _is_ a slow first byte now — under CSR that query happened after paint, in the browser.
- **Complexity.** Hydration has rules: server and client must agree on the output, or React warns about mismatches; window/document don't exist on the server; data fetching must resolve before render. A whole class of bugs CSR simply doesn't have.
- **Caching is the whole game.** Because every request _can_ render, the question is which _should_. SSG and ISR are SSR's answers to "render once" and "render sometimes" — choosing between them per-route is most of the real work.

SSR buys better first paint and real crawlability, and charges you server cost, TTFB (time to first byte) sensitivity, and a stricter way of thinking. Pick it where content is dynamic or SEO-critical; pick SSG/ISR where it isn't.

## The frameworks that make it ergonomic

The technique predates all of them — PHP and Rails rendered HTML on the server before "SPA" (single-page application) was a word. What changed is that React, Vue, and Angular were born as _client_ renderers, and bringing them back to the server required real plumbing: a renderer that runs components in Node, a router that works on both ends, a data-fetching layer that resolves before render, and a hydration step on the client. That plumbing is what the **meta-frameworks** (frameworks built on top of a component library) package up.

```figure
<svg viewBox="0 0 740 240" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="A base layer box labelled component renderer (React, Vue, Angular). Above it a box labelled meta-framework plumbing: routing, data fetching, hydration, caching. Three arrows rise from the plumbing to three meta-framework labels: Next.js for React, Nuxt for Vue, Angular Universal for Angular, plus TanStack Start for React. Each meta-framework defaults to server-rendered HTML.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- base layer -->
    <rect x="60" y="170" width="620" height="50" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="370" y="194" font-size="13" font-weight="700" fill="#1e1b4b" text-anchor="middle">Component renderer that runs on the server</text>
    <text x="370" y="210" font-size="10.5" fill="#475569" text-anchor="middle">React · Vue · Angular — originally client-only</text>

    <!-- plumbing -->
    <rect x="60" y="104" width="620" height="50" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="370" y="128" font-size="13" font-weight="700" fill="#422006" text-anchor="middle">Meta-framework plumbing</text>
    <text x="370" y="144" font-size="10.5" fill="#475569" text-anchor="middle">routing · pre-render data fetching · hydration · caching</text>

    <!-- connectors -->
    <line x1="200" y1="170" x2="200" y2="156" stroke="#94a3b8" stroke-width="1.5"/>
    <line x1="370" y1="170" x2="370" y2="156" stroke="#94a3b8" stroke-width="1.5"/>
    <line x1="540" y1="170" x2="540" y2="156" stroke="#94a3b8" stroke-width="1.5"/>

    <!-- meta-frameworks -->
    <rect x="90" y="30" width="160" height="56" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="170" y="54" font-size="13" font-weight="700" fill="#052e16" text-anchor="middle">Next.js</text>
    <text x="170" y="72" font-size="10" fill="#475569" text-anchor="middle">React · React Server Components</text>

    <rect x="290" y="30" width="160" height="56" rx="8" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="370" y="54" font-size="13" font-weight="700" fill="#500724" text-anchor="middle">Nuxt</text>
    <text x="370" y="72" font-size="10" fill="#475569" text-anchor="middle">Vue · hybrid rendering</text>

    <rect x="490" y="30" width="160" height="56" rx="8" fill="#dbeafe" stroke="#2563eb" stroke-width="1.5"/>
    <text x="570" y="54" font-size="13" font-weight="700" fill="#1e3a8a" text-anchor="middle">Angular Universal</text>
    <text x="570" y="72" font-size="10" fill="#475569" text-anchor="middle">TanStack Start (React)</text>

    <line x1="170" y1="104" x2="170" y2="88" stroke="#94a3b8" stroke-width="1.5"/>
    <line x1="370" y1="104" x2="370" y2="88" stroke="#94a3b8" stroke-width="1.5"/>
    <line x1="570" y1="104" x2="570" y2="88" stroke="#94a3b8" stroke-width="1.5"/>
  </g>
</svg>
```

**Next.js (React).** The one that pushed SSR-by-default into the mainstream for React, and the one this site is built on. Each route picks its rendering mode — SSR for a dashboard, SSG for a blog post, ISR for a list that revalidates on a tag. The App Router pushed the model further with React Server Components, where components run on the server by default and ship only the JS they need [5]. Next made "where does this render?" a per-route question.

**Nuxt (Vue).** Vue's answer to the same problem, and the symmetry with Next is deliberate — file-based routing, a Nitro server engine for Node or the edge (servers close to the user), and "hybrid rendering": a per-route config mixing SSR, SSG, ISR, and SPA in one app [6].

**Angular (Universal).** Took server rendering seriously earliest of the three, via Angular Universal — server-side rendering plus build-time prerendering [7]. Same spectrum; the framework's opinionated structure (DI — dependency injection, its way of handing components what they need — modules, templates) just makes the plumbing feel different.

**TanStack Start (React).** A newer React full-stack framework on TanStack Router, with SSR as a first-class default rather than a bolt-on [8]. The same bet the others are making — routing, server functions, data fetching, and rendering mode in one coherent layer — expressed with TanStack's primitives.

The pattern across all of them: the underlying component library (React, Vue, Angular) is the renderer; the meta-framework decides _when_ to call it and _where_, plus the plumbing — routing, data, hydration, caching. Once I saw it that way, "which framework should I learn" stopped being a question about SSR and became a question about my component library.

## How I use this

The habit these notes left me with is a per-route question instead of a per-app one: _how dynamic is this content, and who needs to read it?_ Static and public (a blog post) → SSG. Static-ish with occasional updates (a project list) → ISR. Per-user or time-sensitive (a dashboard, a cart) → SSR. Interactive-only behind auth where SEO is irrelevant → CSR is honestly fine. SSR isn't the upgrade path I thought it was; it's one option on a slider.

## References

[1] Mozilla, "Client-side rendering (CSR)," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Glossary/CSR](https://developer.mozilla.org/en-US/docs/Glossary/CSR)

[2] Vercel, "Rendering: The four rendering models," Next.js Learn, 2024. [Online]. Available: [https://nextjs.org/learn/dashboard-app/rendering](https://nextjs.org/learn/dashboard-app/rendering)

[3] Mozilla, "Server-side rendering (SSR)," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Glossary/SSR](https://developer.mozilla.org/en-US/docs/Glossary/SSR)

[4] Google, "Understand how search works with JavaScript," Google Search Central documentation, 2024. [Online]. Available: [https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics)

[5] Vercel, "Rendering fundamentals," Next.js Documentation, 2024. [Online]. Available: [https://nextjs.org/docs/app/building-your-application/rendering](https://nextjs.org/docs/app/building-your-application/rendering)

[6] NuxtLabs, "Rendering modes," Nuxt Documentation, 2024. [Online]. Available: [https://nuxt.com/docs/guide/concepts/rendering](https://nuxt.com/docs/guide/concepts/rendering)

[7] Angular Team, "Server-side rendering (SSR) with Angular," Angular Documentation, 2024. [Online]. Available: [https://angular.dev/guide/ssr](https://angular.dev/guide/ssr)

[8] TanStack, "TanStack Start overview," TanStack Documentation, 2025. [Online]. Available: [https://tanstack.com/start/latest/docs/framework/react/overview](https://tanstack.com/start/latest/docs/framework/react/overview)

```quiz
Q: Under pure CSR, what does the browser receive on the first request?
- A fully rendered HTML page with all content
- A near-empty HTML shell plus a JavaScript bundle that builds the UI
correct: 1
explain: CSR ships an empty container and a script. Real HTML appears only after the JS downloads, runs, and writes to the DOM — which is why first paint is late.

Q: What does SSR buy you that CSR does not?
- Smaller JavaScript bundles, automatically
- Content visible on the first paint, and HTML a crawler can read without running JS
correct: 1
explain: SSR returns rendered HTML, so the browser paints content immediately and search engines index it without executing JavaScript. Bundle size is a separate problem SSR doesn't solve on its own.

Q: HTML is generated once at build time and served as static files. That is…
- SSR
- SSG
- ISR
correct: 1
explain: SSG (Static Site Generation) renders at build time. ISR adds revalidation on top of SSG; SSR renders on every request.

Q: Next.js, Nuxt, and Angular Universal are best described as…
- new component libraries that replace React, Vue, and Angular
- meta-frameworks that add routing, data fetching, hydration, and caching on top of an existing component renderer
correct: 1
explain: They layer server-rendering plumbing onto React, Vue, or Angular respectively. The underlying component library is the renderer; the meta-framework decides when and where to call it.

Q: Which is a real cost SSR introduces that CSR avoids?
- The browser has to download JavaScript
- Server CPU spent rendering the component tree on every request, and a slower TTFB when data is slow
correct: 1
explain: SSR moves rendering work onto the server, so traffic scales render cost, and a slow data fetch becomes a slow first byte instead of happening after paint.
```
