Free lesson · Frontend System Design
"We'll build it as an SPA." Why? "Because that's how you build things now."
That exchange - or something like it - kicks off a huge number of projects, and it's exactly the reflex this lesson exists to interrupt. Single-page applications became the default not always because they were the right choice, but because they were the fashionable choice, and "everyone uses React so we'll make an SPA" is an architecture decision made by momentum rather than by reasoning. In an interview, reaching for "SPA, obviously" without engaging the tradeoffs is a quiet red flag. The senior signal is the opposite: knowing that SPA versus MPA is a genuine fork with real costs on both sides, and being able to say for this product, here's which and why.
Nobody framed this tradeoff for me when I was learning - I built SPAs because the tutorials did, and I didn't understand what I'd given up until I shipped one with a brutal first-load time and watched users bounce before the JavaScript even arrived. So let's actually reason about it, because this is one of the highest-leverage architecture decisions on the frontend.
The two models, mechanically
The difference is entirely about what happens when the user navigates to a new "page."
In a Multi-Page Application (MPA) - the traditional model - every navigation is a full round trip to the server. Click a link, the browser throws away the current page, requests a brand-new HTML document for the new URL, and the server sends back fully-formed HTML. Each page is its own document. This is how the web worked for its first couple of decades, and how most content sites still work.
In a Single-Page Application (SPA), the server sends one HTML shell on the first load, plus a JavaScript bundle that takes over. From then on, navigation is intercepted by client-side JavaScript: instead of requesting a new document, the app updates the URL via the History API (the BOM, from last lesson), fetches just the data it needs (usually JSON), and re-renders the relevant part of the page in place. No full reload. The "pages" are an illusion the JavaScript maintains.
sequenceDiagram
participant U as User
participant B as Browser
participant S as Server
Note over U,S: MPA navigation
U->>B: click link to /about
B->>S: GET /about (full request)
S-->>B: complete HTML document
B->>B: discard old page, render new one
Note over U,S: SPA navigation
U->>B: click link to /about
B->>B: router intercepts, updates URL (pushState)
B->>S: GET /api/about-data (JSON only)
S-->>B: JSON
B->>B: re-render in place, no reload
This sequence diagram contrasts navigation in the two models. In an MPA, the user clicks a link to /about, the browser sends a full GET request to the server, the server returns a complete HTML document, and the browser discards the old page and renders the new one. In an SPA, the user clicks the same link, but the client-side router intercepts it and updates the URL using pushState; the browser requests only JSON data from an API endpoint, the server returns that JSON, and the browser re-renders the relevant part of the page in place with no full reload.
The tradeoffs, honestly
Each model wins on different axes, and the whole skill is matching the model to what the product actually needs.
Where SPAs win:
- In-app navigation feels instant. Once loaded, moving between views is fast and app-like - no white flash, no full reload, preserved state. For a tool people use for long sessions (a dashboard, an editor, an email client), this is a genuinely better experience.
- Rich interactivity. Complex, stateful UIs that respond moment-to-moment are natural in an SPA, where the whole app lives in memory.
- Decoupled frontend/backend. The backend becomes a clean API; the frontend is a separate deployable. Teams can move independently.
Where SPAs lose:
- Heavy initial load. The browser must download, parse, and execute a large JavaScript bundle before the app is interactive - and on the slow networks and weak devices the first lesson warned about, that's a real wait, often a blank screen until the JS arrives. The first impression can be the slowest moment.
- SEO and shareability are harder. The initial HTML is an empty shell; content appears only after JavaScript runs. Search crawlers and link-preview bots may see nothing useful unless you add server rendering. For content you want discovered, this is a serious liability.
- You re-implement the browser. Routing, scroll restoration, focus management, the back button, loading states - the browser gave you all of this for free in an MPA, and in an SPA you (or your framework) must rebuild every one of them, often imperfectly.
- More JavaScript to ship and maintain. More bundle, more client-side state, more ways to break.
Where MPAs win: fast first paint (the server sends real HTML, no JS required to see content), excellent SEO (every page is real HTML), simplicity (the browser handles navigation, history, scroll - you don't), and resilience (a page works even if some JavaScript fails).
Where MPAs lose: every navigation is a full reload with its flash and its round trip, in-app interactions feel less fluid, and shared state across pages is awkward because each page starts fresh.
Pros:
SPA - choose when:
- The product is an app people use in long sessions (dashboard, editor, tool)
- In-app navigation speed and rich interactivity matter most
- SEO is not critical (it's behind a login, or internal)
- You can afford to manage the initial-load cost
Cons:
MPA - choose when:
- The product is content people discover and read (marketing, blog, docs, e-commerce listings)
- SEO and fast first paint are critical
- Pages are largely independent
- Simplicity and resilience are valued over app-like transitions
The decision framework
Strip away the fashion and the choice comes down to a few questions:
graph TD
Q1{Is it primarily content to be<br/>discovered & read, or an app<br/>people actively use?}
Q1 -->|content: blog, marketing,<br/>docs, e-commerce| MPA[Lean MPA<br/>or server-rendered]
Q1 -->|app: dashboard, editor,<br/>tool, email| Q2{Is SEO / first-paint<br/>still important?}
Q2 -->|no, it's behind login| SPA[SPA is a fine fit]
Q2 -->|yes, need both| HYBRID[Hybrid:<br/>server-render the SPA]
This decision flowchart starts by asking whether the product is primarily content to be discovered and read, or an app people actively use. If it's content, like a blog, marketing site, documentation, or e-commerce listings, lean toward an MPA or server-rendered approach. If it's an app, like a dashboard, editor, tool, or email client, ask a second question: is SEO and first paint still important? If no, because it's behind a login, an SPA is a fine fit. If yes, you need both app-like interactivity and good first paint, so reach for a hybrid that server-renders the SPA.
Notice the bottom-right branch, because it's where the modern web actually lives: you usually don't have to choose purely. The frameworks that dominate today exist precisely to collapse this tradeoff.
The modern blur: you can have both
The SPA-versus-MPA framing is a little dated as a binary, and saying so in an interview shows you're current. Server-side rendering (SSR) and static site generation (SSG) let you send real, content-filled HTML on the first load - getting the MPA's fast first paint and SEO - and then "hydrate" it with JavaScript so subsequent navigation feels like an SPA. Frameworks like Next.js, Remix, Nuxt, and SvelteKit are built around exactly this hybrid, and newer patterns like islands architecture and React Server Components push it further by shipping less JavaScript while keeping interactivity where it's actually needed.
This means the real question is rarely "SPA or MPA?" in the strict sense. It's "how much should the server render up front, and how much should the client take over after?" - a spectrum from fully static, through server-rendered-then-hydrated, to fully client-rendered. We dedicate proper time to that spectrum in the rendering-patterns lesson of the performance module. For now, the point is: you should know the pure SPA and MPA models cold, and know that production frontends usually blend them deliberately.
Important: The trap to avoid in a round is treating "SPA" as automatically modern and "MPA" as automatically outdated. The genuinely current take is that the binary itself is mostly obsolete - real systems pick a point on the SSR/SSG/CSR spectrum based on whether they need discoverability and fast first paint (push work to the server) or rich in-session interactivity (push work to the client), and frameworks let you mix per-route. Saying that demonstrates you've kept up.
Interview: When a system design prompt is a content product (news feed, e-commerce, marketing), proactively raise SEO and first paint and argue for server rendering. When it's an app behind a login (a dashboard, an internal tool), argue that SEO doesn't matter and an SPA's interactivity and decoupling win. Either way, state the product type first, then derive the architecture from it - that ordering is the senior move. Choosing the architecture before identifying the product type is exactly the momentum-driven mistake interviewers are listening for.
Why SEO actually breaks in an SPA (the mechanism)
"SPAs are bad for SEO" gets repeated a lot without explanation, and the explanation is worth having because it's both an interview point and the thing that tells you when the problem matters and when it doesn't.
When a search crawler or a social link-preview bot requests an SPA's URL, the server returns the shell: a near-empty HTML document with a <div id="root"></div> and a script tag. The actual content only materializes after the JavaScript downloads, executes, fetches data, and renders. So the question becomes: does the bot run the JavaScript and wait for the content?
The honest answer is "sometimes, partially, slowly." Major search engines can execute JavaScript, but they do it on a deferred, resource-constrained "second pass," which can delay indexing and isn't guaranteed to capture everything - especially content gated behind interactions or slow data fetches. Many smaller crawlers and most social-preview bots (the ones generating the card when you paste a link into a chat app) don't run JavaScript at all - they read the raw HTML, see the empty shell, and produce a blank or broken preview. So an SPA's content can be invisible exactly where discoverability matters most.
This is precisely the problem server-side rendering solves: by sending real, content-filled HTML on that first request, the crawler and the preview bot get the content immediately, no JavaScript execution required. The page can then hydrate into an SPA for the human. So "SPAs are bad for SEO" really means "client-only rendering is bad for SEO, and server rendering fixes it" - which is why content products reach for SSR or SSG rather than abandoning the SPA model entirely.
Note: The decision rule that falls out: if the content needs to be found (public marketing, articles, product pages, anything you want in search results or link previews), you need real HTML on first load - server-rendered or static. If the content lives behind a login (a dashboard, an internal tool, an authenticated app), crawlers never see it anyway, so client-only rendering carries no SEO penalty. The product's discoverability requirement decides it - which is one more reason to identify the product type before picking the architecture.
The hidden cost of hydration
The hybrid approach - server-render for first paint, then hydrate into an SPA - sounds like a free lunch, and it mostly isn't, so knowing the catch is a strong signal. Hydration is the process where the client-side JavaScript "adopts" the server-rendered HTML: it downloads, parses, and runs the same component code, attaches event listeners, and reconstructs the in-memory state so the page becomes interactive.
The catch is the uncanny valley between visible and interactive. With SSR, the user sees content quickly (great first paint), but the page isn't actually clickable until hydration finishes - and hydration requires downloading and executing roughly the same amount of JavaScript an SPA would. So there's a window where the page looks ready but doesn't respond: tap a button and nothing happens because the listeners aren't attached yet. On a slow device, that window can be uncomfortably long, and it's a frustrating experience precisely because the page looks finished.
This is why the cutting edge keeps trying to ship less JavaScript: islands architecture hydrates only the interactive bits and leaves static content as plain HTML; React Server Components render some components entirely on the server so their code never ships to the client at all; streaming SSR sends HTML progressively. Each is an attempt to keep SSR's fast first paint while shrinking the hydration cost. You don't need to implement these in a round, but naming hydration as the real cost of the hybrid model - and these as the responses to it - is the kind of current, mechanism-level understanding that separates a senior answer from a buzzword.
Comparing the models on the metrics that matter
Tying this back to the rendering-pipeline metrics from earlier in the module makes the tradeoff concrete:
| Metric | MPA | SPA (client-only) | Hybrid (SSR + hydrate) |
|---|---|---|---|
| First Contentful Paint | Fast (real HTML) | Slow (waits for JS) | Fast (server HTML) |
| Time to Interactive | Fast per page | Slow first load, fast after | First paint fast, interactive after hydration |
| In-app navigation | Full reload each time | Instant | Instant after first load |
| SEO / link previews | Excellent | Poor without extra work | Excellent |
| Initial JS payload | Minimal | Large | Large (but content shows first) |
Read across the "Hybrid" column and you can see why it became the default for serious products: it takes the MPA's wins on first paint and SEO and the SPA's win on navigation, paying for it with payload size and hydration complexity. There's no free lunch - just a deliberate placement on the spectrum based on what the product needs most.
What you should carry out of this lesson
An MPA does a full server round trip per navigation, sending complete HTML each time - great for first paint, SEO, and simplicity, weaker for fluid in-app transitions. An SPA loads one shell plus a JavaScript bundle that intercepts navigation and re-renders in place - great for app-like interactivity, weaker on initial load, SEO, and the pile of browser behaviors it forces you to reimplement. The choice follows the product: content to be discovered leans server-rendered; an app used in long sessions leans client-driven. And the modern reality is a spectrum, not a binary - frameworks let you server-render for the first paint and hydrate into SPA-like behavior afterward.
That completes the core conceptual map of how the browser and web really work. The last batch of this module gets concrete and practical: the build-time transformations that shrink and clean up the JavaScript and CSS you ship - minification, compression, tree shaking, dead-code elimination - and a careful look at CSS positioning through the lens of the layout and rendering costs you now understand. Those are the tools that make the difference between shipping a lean app and shipping a bloated one.