Skip to content

SPA vs SSR vs SSG

Key Points

  • Rendering models are a spectrum, not a binary. SPA, classic SSR, SSG, ISR, streaming SSR, RSC, hybrid — pick per route, not per app.
  • TTFB ≠ FCP ≠ LCP ≠ TTI. Each model optimizes different metrics. Knowing which matters for your audience drives the choice.
  • Hydration is the hidden cost of SSR/SSG: the browser re-runs the same component tree to attach event listeners, doubling JS work on first load.
  • Blazor's render modes map cleanly: WebAssembly ≈ SPA, Server ≈ stateful SSR over SignalR, Static SSR ≈ classic SSR, Auto ≈ progressive enhancement.
  • MVC is classic SSR. There's nothing wrong with it for content-heavy or workflow apps. SPA is not the default answer.
  • CDN edge + SSG/ISR wins on raw cost. Origin SSR pays per request.

Concepts (deep dive)

Rendering models, end to end

Pure SPA (Single-Page Application)

Browser ──GET /──▶  CDN/Server
        ◀─ index.html (near-empty shell + bundle.js) ─

Browser parses HTML, downloads & runs bundle.js
Browser ──GET /api/data──▶ API
        ◀─ JSON ─
Browser renders DOM client-side.

TTFB: fast (static shell). FCP/LCP: slow (waits for JS). TTI: slow. SEO: bad without prerender. Cost: cheap (CDN-only for shell).

Classic SSR (Server-Side Rendering)

Browser ──GET /─▶ App Server ──query──▶ DB
                            ◀─ rows ─
        ◀─ fully-rendered HTML ─
Browser displays immediately.
[optional] downloads JS, hydrates → interactive.

TTFB: depends on origin + DB. FCP/LCP: fast — HTML arrives painted. TTI: depends on hydration. SEO: great. Cost: per-request server cost.

ASP.NET Core MVC, Razor Pages, classic Rails, classic Django, Next.js getServerSideProps.

SSG (Static Site Generation)

Build time:  Generator ──▶ HTML files for every route ──▶ CDN
Request time: Browser ──GET /about──▶ CDN ──HTML──▶ Browser

TTFB: fastest (CDN edge). FCP/LCP: fastest. SEO: great. Cost: cheapest. Trade-off: stale unless rebuilt; doesn't fit user-specific content.

Hugo, Jekyll, Astro, Next.js getStaticProps, Gatsby. (This study guide is itself MkDocs SSG.)

ISR (Incremental Static Regeneration)

SSG with a TTL. First request after expiry triggers a background regenerate; visitor still gets the cached version.

Build:  generate /products/123.html
T+0..N: serve cached version
T>N:    next request → serve cached, background-revalidate
T>N+ε:  subsequent requests → serve fresh

Fits e-commerce: product pages stale-while-revalidate every few minutes, no full rebuild.

Streaming SSR

Server starts flushing HTML as soon as the shell is ready. Slow data sections render <Suspense> placeholders inline; the chunks for those arrive later in the same response.

Server flushes:
  <html>...<header/>...<Suspense fallback>spinner</Suspense>...
        ↓ (data arrives)
  <template id="s1">real content</template><script>$RC("s1")</script>

Improves perceived performance: critical above-the-fold content paints while slow widgets stream in. React 18+, Next.js App Router.

React Server Components (RSC)

Components that never ship JS to the client. They run on the server, return a serialized component tree, and intersperse with client components for interactivity.

Server:  <Page>  (RSC, no JS)
            <Header/>             (RSC)
            <ClientCounter/>      (client component, hydrates)
            <Comments/>           (RSC, fetches DB direct)

Wins: zero JS for static parts; direct DB access without API endpoints. Cost: new mental model; debugging the server/client boundary takes care.

Hybrid (per-route choice)

Modern frameworks (Next.js, Remix, SvelteKit, Astro) let you pick per route: marketing pages SSG, dashboard SSR, settings client-rendered. This is the right answer for most products.

Performance metrics

Metric What it measures Target
TTFB Time to first byte (server response start) < 800ms
FCP First Contentful Paint — first text/image < 1.8s
LCP Largest Contentful Paint — biggest hero element < 2.5s
TTI Time To Interactive — main thread idle < 3.8s
INP Interaction to Next Paint (replaces FID) < 200ms
CLS Cumulative Layout Shift — visual stability < 0.1

These are Google's Core Web Vitals. They affect SEO ranking.

Hydration cost

Server sends pre-rendered HTML. Browser displays it instantly. Then the JS bundle arrives, the framework re-runs every component to wire up event handlers, and only then is the page actually interactive. Until that finishes, clicks do nothing.

   FCP/LCP            TTI
     │ (paint)         │ (interactive)
     ▼                 ▼
─────●─────────────────●──────►
     │  hydration time │
     └─── still ~JS-blocked ───┘

Mitigations: - Partial hydration / islands (Astro): hydrate only interactive components. - Resumability (Qwik): serialize state, skip re-execution. - RSC: components that never need hydration at all. - Smaller bundles (the perennial answer).

SEO implications

  • SPA without prerender → crawlers see empty <div id="root">. Google can JS-render but slowly and inconsistently. Bing/Yandex/social-card crawlers usually can't.
  • SSR / SSG → fully-rendered HTML, indexable instantly.
  • Open Graph / Twitter Card scrapers do not run JS. Marketing pages must be SSR or SSG.

Cost & infrastructure

Model Hosting Per-request cost Cold-start risk
SSG CDN only Cents/million None
SPA CDN (shell) + API origin Mostly API API only
Classic SSR App server + DB Server CPU + DB High if serverless
ISR CDN + occasional regen Mostly CDN Background
Streaming SSR App server (long-lived flush) Server CPU + holds connection Higher tail latency

Edge runtimes (Cloudflare Workers, Vercel Edge, Azure Front Door) collapse some categories — SSR at the edge approaches CDN economics if your data layer is co-located.

Mapping Blazor render modes

Blazor render mode Closest taxonomy Notes
Static SSR (default in Blazor 8/9) Classic SSR Pure HTML response; no interactivity unless added
Interactive Server Stateful SSR over SignalR UI runs on server, diffs sent over WebSocket. Latency-sensitive.
Interactive WebAssembly SPA .NET runtime + assemblies download to browser; runs locally
Interactive Auto Progressive enhancement Starts on server (fast first load), upgrades to WASM in background

ASP.NET Core MVC and Razor Pages = Classic SSR. Same mental model as Rails or Django. Don't apologize for it; it ships pages fast and is great for forms-heavy line-of-business apps.

Decision matrix

Requirement Pick
Marketing site, mostly static, perfect SEO SSG (Astro, Hugo, Next static)
Content site with frequent updates ISR or SSG with webhook rebuilds
Internal dashboard, auth-walled, no SEO SPA (React, Angular, Blazor WASM)
Public catalog with search/filter SSR or streaming SSR
Server-tracked workflow (CRUD + forms) MVC / Razor Pages (classic SSR)
App needing offline support SPA + service worker (PWA)
Real-time collaborative app Blazor Server / SPA + SignalR/WS
App with low-end mobile audience SSR + minimal JS or islands
Mixed (some marketing, some app) Hybrid — per-route choice

Picking by team

  • .NET-only team, no JS loveMVC or Blazor Server.
  • .NET team that wants modern interactive UX → Blazor Auto or Interactive Server.
  • JS-heavy team with ASP.NET API → React/Angular SPA + minimal-API backend.
  • Marketing-led team → Astro / Next.js + headless CMS, .NET API where needed.

Code: correct vs wrong

❌ Wrong: SPA shell for a marketing landing page

<!-- index.html -->
<div id="root"></div>
<script src="/bundle.js"></script>

LCP suffers; OG scrapers see nothing; SEO penalized.

✅ Correct: SSG or SSR for marketing

@page "/"
<h1>@Model.Headline</h1>
<p>@Model.SubHeadline</p>

Or with Astro / Next.js static export. HTML is in the response.


❌ Wrong: SSR for an internal dashboard behind auth

Server pays CPU per render, no SEO benefit, hydration still required.

✅ Correct: SPA (or Blazor WASM)

CDN-served shell, JSON API, no per-request render cost.


❌ Wrong: Pure SSR with a 4-second DB query blocking the whole page

User stares at a white screen until the slow query returns.

✅ Correct: Streaming SSR with <Suspense>

<Page>
  <Header />
  <Suspense fallback={<Skeleton />}>
    <SlowWidget />
  </Suspense>
</Page>

Shell paints immediately; slow part streams in.


Design patterns for this topic

Pattern 1 — "Per-route render strategy"

  • Intent: marketing routes static, app routes SPA, mixed routes SSR. Pick per page.

Pattern 2 — "Islands of interactivity"

  • Intent: static HTML page with isolated JS-hydrated components. Astro's model. Lowest TTI for content sites.

Pattern 3 — "ISR for catalog data"

  • Intent: product/article pages cached statically, regenerated in the background on a TTL.

Pattern 4 — "SSR shell + client data fetch"

  • Intent: server renders the layout fast; client fetches user-specific data after hydration. Splits cacheable from personal.

Pattern 5 — "Edge-rendered SSR"

  • Intent: run SSR on a CDN edge runtime, co-locate read replicas. Approaches SSG economics with SSR flexibility.

Pros & cons / trade-offs

Aspect Pros Cons
SPA Rich interactivity, cheap hosting Slow first paint, SEO without prerender, hydration cost on initial route
Classic SSR Great SEO, fast LCP Per-request server cost, hydration tax for interactive bits
SSG Cheapest, fastest, best SEO Stale unless rebuilt, no user personalization
ISR SSG speed + freshness Cache-invalidation complexity
Streaming SSR Fast TTFB, progressive paint Complex error handling, long-lived connections
RSC Zero JS for static parts, server data access New mental model, framework-locked
Blazor Server No client .NET install, real-time Latency-sensitive, scales by connection count

When to use / when to avoid

  • Use SSG/ISR for marketing, blogs, docs, product catalogs.
  • Use SPA for auth-walled dashboards and tool-style apps.
  • Use SSR (classic or streaming) for SEO-critical dynamic pages.
  • Use MVC/Razor for forms-heavy line-of-business apps. It's still a great hammer.
  • Avoid SPA-by-default for content sites — you're paying SEO and FCP costs for no benefit.
  • Avoid SSR everywhere when half your pages don't change per user.

Interview Q&A

Q1. SPA vs SSR — biggest trade-off? SPA: cheap hosting, slow first paint, SEO challenges. SSR: fast first paint and SEO, server CPU per request.

Q2. What is hydration? The browser re-runs the component tree on top of server-rendered HTML to attach event handlers and make it interactive.

Q3. SSG vs ISR? SSG: rebuild every change. ISR: SSG with TTL — stale-while-revalidate in the background. ISR fits content that updates without warning.

Q4. Why streaming SSR? Slow data sections don't block the shell. The server flushes HTML as it's ready; placeholders stream in later. Better LCP and perceived perf.

Q5. Blazor render modes mapping? Static SSR ≈ classic SSR. Interactive Server ≈ stateful SSR over WebSockets. Interactive WASMSPA. Auto ≈ progressive enhancement (server first, WASM upgrade).

Q6. What problem do React Server Components solve? Static UI parts shouldn't ship JS or require an API. RSC runs them on the server, returns serialized component output, no client bundle for them.

Q7. TTI vs LCP? LCP: biggest above-the-fold element painted. TTI: main thread idle, page actually responds to clicks. SSR can have a great LCP and a poor TTI due to hydration.

Q8. INP — what changed? INP (Interaction to Next Paint) replaced FID in March 2024 as a Core Web Vital. Measures the slowest interaction over the page lifetime, not just the first.

Q9. When does MVC still make sense? Forms-heavy LOB apps, internal tools, server-tracked workflows. Lower complexity than SPA, no hydration tax. Don't pick SPA reflexively.

Q10. Cost — SSR vs SSG? SSG runs CPU once at build, then pays only CDN bandwidth. SSR pays server CPU every request. Edge runtimes narrow the gap but don't close it.

Q11. SEO with a SPA — options? Server-side prerender (puppeteer-style) for crawlers, switch to SSG/SSR, or use a meta-framework that supports per-route SSR (Next.js, Remix, Nuxt).

Q12. Why "islands"? Most of a content page is static HTML. Hydrate only the interactive widgets (search, comments). Astro popularized this; massively reduces TTI.


Gotchas / common mistakes

  • ⚠️ SPA-by-default for content sites — ships hundreds of KB of JS to render text. SSG is right.
  • ⚠️ Hydration mismatches — server HTML differs from client first render → React/Blazor warnings, layout flicker. Keep server and client deterministic.
  • ⚠️ window/document access during SSR — undefined on server. Guard with environment checks or framework hooks.
  • ⚠️ SignalR connection scaling — Blazor Server holds a connection per tab. Provision SignalR backplane (Redis/Azure SignalR) for >1k concurrent.
  • ⚠️ ISR cache-invalidation — forgetting to revalidate on writes leaves stale prices. Wire revalidation hooks to your write paths.
  • ⚠️ Streaming SSR error handling — once headers are flushed, you can't return a 500. Design fallback UI inside <Suspense> boundaries.
  • ⚠️ OG scrapers don't run JS — Twitter/Slack/LinkedIn cards from a SPA: empty. SSR/SSG the share-target page.
  • ⚠️ Blazor WASM bundle size — first visit can be MB-scale. Use AOT and trimming; offer a Blazor Auto path for fast initial loads.

Further reading