SPA is not CSR: the 2026 rendering strategy map

frontendspacsrssrssgrscrendering

For years I thought SPA meant CSR. Same thing, different acronym. Then Next.js shipped SSR and I assumed it replaced SPA. Then RSC arrived and the whole vocabulary shuffled again. Every wave conflated with its predecessor.

The cleanest mental model:

  • SPA is an architecture. It describes the user experience: one HTML document, in-page updates via JS, no full page reloads on navigation.
  • CSR / SSR / SSG / RSC / Islands / Resumability are rendering strategies. They describe where and when HTML gets assembled, and what the browser receives.

These are independent axes. You can build an SPA with CSR, with SSR, with SSG, with all three combined per route. The SPA-feel is decided by the navigation model, not by where the initial HTML comes from.


What an SPA actually is

The defining trait: navigation is handled by the browser, not the server.

Traditional MPA:
  Click link → browser sends GET → server returns full HTML →
    browser unloads current page → renders new page → JS re-runs

SPA:
  Click link → router intercepts → fetch data (maybe) →
    update DOM in place → URL updated via history.pushState →
    page never unloaded

The user perceives "fast, no white flash, app-like". The server perceives "this client is asking for specific data, not whole pages".

This says nothing about how the initial page got rendered. That's a separate question.


The three classic rendering strategies

CSRSSRSSG
HTML built whereBrowserServer (on request)Server (at build time)
HTML built whenAfter JS loadsPer requestDuring npm run build
Initial HTML contentMostly empty (<div id="root"></div>)Full markupFull markup
Server needs at runtimeStatic file serverNode / runtimeStatic file server (CDN)
Per-request costZero on serverRender cost per requestZero on server
Personalized contentEasy (fetch after mount)Easy (render with request data)Hard (must client-fetch or use ISR)

The user-facing difference:

  • CSR: browser receives skeleton, JS bundles arrive, JS executes, JS renders. Blank-screen window until JS finishes.
  • SSR: browser receives full HTML, paints immediately, JS arrives later and "hydrates" to make it interactive.
  • SSG: same as SSR from the browser's view, but the HTML was baked at build time and served from a CDN.

TTFB / FCP / TTI: the three metrics that actually differ

MetricWhat it measuresCSRSSRSSGPure server-render (no SPA)
TTFB (Time To First Byte)Server response startFastest (static file)Slowest (server must render)Fastest (CDN)Slow (server must render)
FCP (First Contentful Paint)First visible contentSlow (wait for JS)Fast (HTML paints immediately)Fast (HTML from CDN)Fast (HTML is final)
TTI (Time To Interactive)First moment user can clickSlow (wait for JS mount)Slow (wait for hydration)Slow (wait for hydration)Fast (no hydration needed)

The interesting cell is bottom-right. Pure server-render (Rails ERB, Django templates, PHP, Laravel Blade) has no hydration, so TTI catches up with FCP almost instantly. The trade-off is no SPA-feel: every link click is a full page reload.

This is why "SPA framework" is a tax bargain: pay hydration cost once, get smooth navigation thereafter. Pure server-render skips hydration but pays page-load cost on every click.


Hydration: what it is, what it isn't

Hydration is attaching JS behavior to existing server-rendered HTML. The DOM is already painted. The framework walks the tree, matches it against its component tree, and wires up event handlers, state, refs.

It is not rendering. The HTML doesn't get redrawn. The user-visible content was already there from the server's response.

Frameworks that hydrate: React, Vue, Svelte, Solid, Angular, Lit. Pretty much any modern framework that supports SSR.

Frameworks that don't hydrate:

  • Pure server-render templates (Rails ERB, Django, PHP). The HTML is already final. JS, if any, is progressive enhancement layered separately.
  • Qwik. State is serialized into HTML attributes. When interaction happens, Qwik fetches just the needed code, no full-tree wakeup.

A subtle point: CSR has no hydration either. In CSR, the browser receives a near-empty HTML. JS arrives, React (or Vue, etc.) does mounting: building the DOM from scratch in the browser. "Mount" and "hydrate" are different operations. Mount writes new nodes; hydrate adopts existing nodes. People mix these up because both happen "after JS arrives", but the work the framework does is different.


The post-2023 wave

The CSR / SSR / SSG split was complete by ~2019. Since then, a new layer has emerged. These don't replace the three classics, they sit on top.

RSC (React Server Components)

Default in Next.js App Router since 13. The split:

  • Server components render on the server, never ship JS to the client. They can await directly, fetch DB, use server-only secrets.
  • Client components (marked "use client") ship as JS to the browser and hydrate normally.

The mental model is per-component decision about where it runs, not per-page. A page can mix server and client components freely.

RSC is not "SSR with extra steps". It's a different model: SSR ships the whole tree as JS + HTML; RSC ships the server tree as a serialized RSC payload (JSON-like format), with client components interleaved as hydration islands.

Streaming SSR

Server doesn't wait for all data before sending HTML. Renders what it has, sends those bytes, continues rendering, sends more. The browser paints progressively.

React 18+ renderToPipeableStream. Suspense boundaries mark "this part can be deferred". Without Suspense, you don't get the streaming benefit.

Without streaming:
  request → wait 800ms for slowest query → send full HTML → paint

With streaming:
  request → 100ms: send shell + above-fold → paint immediate
                → 300ms: send middle section → paint
                → 800ms: send footer (with slow data) → paint

Islands architecture

Astro is the canonical example. The page is mostly static HTML by default. Components marked as interactive ("islands") hydrate independently. The rest never ships JS.

Astro page:
  <article>
    <Heading />              ← static, no JS
    <Content />              ← static, no JS
    <CommentBox client:load /> ← island, hydrates
    <Sidebar />              ← static, no JS
  </article>

Trade-off: less JS shipped, faster TTI, but interactive components don't share state easily across islands.

Resumability (Qwik)

Pure no-hydration model. The server sends HTML with state serialized as attributes:

<button on:click="app_TPL_aBc123.js#handleClick">Click</button>

When the user clicks, Qwik fetches app_TPL_aBc123.js, runs the handler, updates DOM. No upfront hydration cost. TTI ≈ FCP.

The cost: cold-start latency on first interaction (need to fetch the handler). Optimized via preloading and code-splitting, but the architecture is fundamentally lazy.

Edge rendering

Running SSR or SSG on edge functions (Cloudflare Workers, Vercel Edge, Deno Deploy, AWS Lambda@Edge). Same rendering model as SSR/SSG, different runtime location: close to the user, low latency, no cold start (or minimal).

Edge runtimes have limits: no Node APIs, smaller memory, no long-running processes. So edge SSR is a constrained subset of "normal" SSR.


The orthogonal axes

Two questions, each with its own answer:

AxisChoices
Navigation experienceSPA (in-page updates) vs MPA (full page reloads)
Initial render strategyCSR / SSR / SSG / RSC / Islands / Resumability

You pick one from each axis. Common combinations:

  • SPA + CSR: classic React/Vue/Angular SPA (pre-2020)
  • SPA + SSR + hydration: Next.js Pages Router, Remix, SvelteKit, Nuxt
  • SPA + SSG + hydration: Gatsby, classic Next.js export
  • SPA + RSC + selective hydration: Next.js App Router (2023+)
  • MPA + pure server-render: Rails, Django, PHP, Laravel
  • MPA + Islands: Astro (default mode), Marko

Pure server-render + SPA doesn't exist meaningfully. If you want SPA navigation, something on the client has to intercept clicks, which requires JS, which means hydration or its equivalent. Astro can layer SPA-feel via View Transitions API but it's hybrid.


How to choose

Use caseStrategy that fits
Personal blog, docs site, marketingSSG or Astro (mostly static, fast, cheap)
E-commerce, news site (SEO + dynamic content)SSR or SSG with ISR
Internal dashboard (logged-in users, heavy interaction)CSR is fine, SEO doesn't matter
App with mixed static and dynamic contentRSC (Next.js App Router)
Performance-critical page with one heavy interactive widgetAstro Islands
App where TTI is the metric to beatQwik (Resumability)
Global low-latency contentEdge SSR or edge SSG

Don't pick a framework by the rendering strategy alone. Pick by team familiarity, ecosystem, hosting cost, and which trade-offs you can tolerate. The rendering strategy is one input among several.


Why this gets so tangled

History compresses categories. The labels arrived in waves:

2010-2014  SPA emerges (Backbone, Angular 1, Ember)
           Default rendering: CSR. So "SPA = CSR" in practice.

2014-2018  React + Vue dominate. Still SPA. Still CSR.
           "SPA" and "CSR" become synonyms in common usage.

2016-2020  Next.js + Nuxt + Gatsby ship SSR / SSG.
           SPA still survives because navigation is still client-side.
           But "SPA = CSR" breaks. People didn't catch up immediately.

2023+      React Server Components.
           Now even within one page, parts are server-only and parts
           are client. The classic three-axis labels become insufficient.

Every term carries the implicit assumptions of when it was coined. "SPA" assumed CSR because that's the era it was born in. "SSR" implied "with hydration" because the React/Vue/Svelte tradition shaped how the word was used.

Read any rendering term as "what people meant by it in 201X" and a lot of confusion dissolves. The vocabulary lags the architecture by about 2 years.


References