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 love → MVC 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
LCP suffers; OG scrapers see nothing; SEO penalized.
✅ Correct: SSG or SSR for marketing
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>
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 WASM ≈ SPA. 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/documentaccess 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.