Choosing a Frontend Architecture
MPA, SPA, SSR application, static site and micro frontends compared on team shape, content versus application, discoverability, interactivity depth and deployment independence — with no universal winner.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
Given this product, this team and these users, which frontend architecture is the right one — and what actually decides it?
A person wants to do a thing: read a policy document, file an expense, edit a floor plan, check an order. They never think about architecture. They feel it as how fast the first screen arrives, whether the back button works, and whether the page still functions on the train.
Start a single-page application. It is what modern frontends are, every job posting names one, and the alternative feels like going backwards to full page reloads.
A documentation site built as a client-rendered SPA ships a router, a framework and a data layer so that a person can read text that never changes. The bytes are pure overhead, and the first screen waits on all of them (Client-Side Rendering).
- A documentation site built as a client-rendered SPA ships a router, a framework and a data layer so that a person can read text that never changes. The bytes are pure overhead, and the first screen waits on all of them (Client-Side Rendering).
- A deeply interactive editor built as a classic multi-page app throws away in-memory document state on every navigation, so every "screen" has to become a modal to avoid losing work.
- A team of forty across six product areas sharing one SPA bundle discovers that no team can ship without the others: one build, one deploy, one release train, and a merge queue that is now the bottleneck (Deploying a Frontend).
- A three-person team that copied a micro-frontend architecture from a conference talk now maintains six repositories, a shell application, a shared-dependency policy and a composition layer to serve a product with eleven screens.
- "SPA because it is modern" is not a criterion — it is a preference with no failure condition attached, which is why it survives contact with evidence. The criteria below all have failure conditions.
What is actually happening
In the browser, not in the framework.
- Every one of these architectures answers exactly two questions: where does the HTML for a screen come from, and who owns navigation — the browser, or your JavaScript.
- MPA. The server returns a complete document per URL. The browser owns navigation: history, scroll, focus, the loading indicator and per-page resource loading are all its job (MPA vs SPA).
- SPA. The server returns one shell document; JavaScript renders every screen and intercepts every link. Your code now owns navigation, and everything the browser used to do for free is yours to reimplement (Client-Side Routing).
- SSR application. The server renders the HTML for the requested URL, then client JavaScript takes over the already-rendered markup and subsequent navigation. Two runtimes produce the same UI, which is a capability and a class of bug (Hydration Mismatch).
- Static site. HTML is produced at build time and served as files from a CDN. There is no per-request server work at all, which makes it the cheapest and least dynamic option (Static Site Generation).
- Micro frontends. Independently built and deployed frontend units composed into one page or one shell at build time or run time. An organisational boundary made technical (Micro Frontends).
- These are not exclusive. A meta-framework will happily give you a statically generated marketing page, a server-rendered product page and a client-rendered application shell inside one deployment, and the honest answer for many products is "different architectures per route" (Choosing a Rendering Strategy).
What this makes the browser do
And which of it is avoidable.
- An MPA makes the browser parse a fresh document per navigation, but only the resources that document actually needs — and it discards the old document's memory entirely, which is a leak that cannot happen (Memory Leaks).
- A SPA makes the browser parse and compile the whole application before the first screen is interactive, then keeps every route's objects, listeners and caches alive for the life of the tab (The Real Cost of JavaScript).
- An SSR application makes the browser do both: parse server HTML, then download and execute the client bundle that adopts it (Hydration).
- A static site makes the browser do the least: fetch a document from an edge cache, parse it, paint. Whatever JavaScript you then add is a deliberate addition rather than a precondition.
- Micro frontends add composition work no single-owner architecture pays: multiple bundles, multiple framework runtimes if they disagree, and duplicated shared dependencies that the browser must download, parse and keep in memory once per copy.
Five shapes, and the two questions that separate them
Strip the marketing from these five and they differ on two axes only: where the HTML for a given screen is produced, and whether the browser or your JavaScript owns navigation. Everything else — bundle size, SEO behaviour, state preservation, deployment coupling — falls out of those two answers rather than being an independent property you can choose separately.
That is why the decision is worth making explicitly. If you pick "SPA" you have also picked "my code owns history, scroll restoration, focus reset, loading indication and per-route code loading", whether or not anyone on the team has agreed to own those.
- Multi-page application — server produces a document per URL, browser owns navigation. You inherit history, scroll, focus and per-page loading for free.
- Single-page application — server produces one shell, JavaScript produces every screen and owns navigation. You inherit nothing and preserve everything.
- Server-rendered application — server produces the first document, client JavaScript adopts it and owns navigation from then on. Both runtimes must agree on the output (Hydration Mismatch).
- Static site — HTML produced at build time, served from a CDN, no per-request server work at all (CDN Delivery).
- Micro frontends — several independently deployed units composed into one experience, each owning its own build and release (Micro Frontends).
Where should the HTML for a screen come from, and who should own navigation?
when Content-dominant product, screens that do not need to preserve in-memory state across navigation, a team that is comfortable with server templates, and a strong requirement that every URL works without executing your JavaScript.
cost A network round trip per navigation, no preserved client state, and richer transitions are hard. Interaction-heavy screens become large islands or modals to avoid navigating away.
when The product is genuinely an application: a long-lived working session, in-memory state that must survive moving between screens, and interaction depth that a document model fights.
cost You reimplement history, scroll restoration, focus management, loading indication and code splitting, and you usually do it incompletely. First load pays for the whole application before any screen is usable (MPA vs SPA).
when You need meaningful HTML on the first response — discoverability, link previews, a usable first screen on a slow device — but the product is still an application afterwards.
cost A server on the critical path that you now operate and scale, two runtimes that must produce identical output, and a window where the page looks interactive but is not (Hydration).
when Content changes on a build cadence rather than a request cadence: documentation, marketing, changelogs, most blogs. Personalisation, if any, is a small client-side addition.
cost No per-request logic anywhere, so anything dynamic needs a separate service. Content changes require a build and a deploy, and a large site's build time becomes the publishing bottleneck (Static Site Generation).
when Multiple teams genuinely need to deploy independently against separate release cadences, and the coupling cost of one build is a measured, current bottleneck rather than an anticipated one.
cost Duplicated dependencies downloaded and parsed once per unit, a composition layer to build and operate, and page-global concerns — design consistency, focus order, landmarks, routing — that no single team owns (Micro Frontends).
The criteria are the lesson
There is no row in this table that reads "best". Each criterion has a different winner, and a product that scores every criterion as equally important has not thought about the product. The work is deciding which two criteria dominate, and then accepting what the other three cost.
Read the table as "what does this architecture make cheap, and what does it make expensive", not as a scoreboard. An architecture that makes the wrong thing cheap for your product is not a bad architecture; it is a mismatch, and it will show up as a team that fights its own tooling on every feature.
| Criterion | MPA | SPA | SSR app | Static site | Micro frontends |
|---|---|---|---|---|---|
| Team shape it suits | One team, or teams split by route on the server | One team, or a few with a shared build | One team that can also operate a server | One team, content authors outside it | Several teams that must release independently |
| Content vs application | Content-leaning; applications become large single screens | Application-leaning; content pays a bundle it does not use | Both, at the cost of running both | Content only, by definition | Neutral — it is an ownership answer, not a rendering one |
| Discoverability and shareability | Every URL is real HTML from the server | Nothing exists until script runs, unless you add rendering back | Every URL is real HTML, then becomes an app | Every URL is a file; the strongest case | Depends entirely on how units are composed |
| Interactivity depth | Shallow to medium; state resets on navigation | Deepest; state survives everything until the tab closes | Deep after hydration completes | Whatever you add on top, deliberately | Deep within a unit, awkward across units |
| Deployment independence | Per route, if the server is split that way | None — one build, one deploy, one release train | None, plus a server release to coordinate | None, but deploys are cheap and instantly revertible | The whole point; also the whole cost |
How this decision actually goes wrong
The failures below are not hypothetical shapes; they are the recurring ways teams arrive at an architecture that nobody would have chosen on the merits. Every one of them is a decision made for a reason that is real but is not a criterion — familiarity, fashion, a previous scar, or an org chart.
The response column is deliberately small. Most of these do not need a rewrite; they need the criterion named out loud, and then a change scoped to the one route or one boundary where the mismatch actually hurts.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| Defaulting to a SPA for a content product | Slow first screen, blank page while the bundle loads, weak link previews, and a large bundle for pages that never change | A client-rendered shell must execute JavaScript before any content exists, and the bundle is a precondition rather than an enhancement | Move content routes to static or server-rendered output and keep the client application for the routes that earn it (Choosing a Rendering Strategy). |
| Defaulting to an MPA for an editor or dashboard | Everything is a modal, work is lost on navigation, and "are you sure you want to leave" appears on every screen | In-memory state does not survive a document navigation, so the architecture pushes every stateful flow into a single page | Keep the MPA for the surrounding product and make the editor a genuine client application on its own route (Who Owns This State?). |
| Micro frontends adopted before the coupling was measured | Six repositories, a shell nobody owns, duplicated frameworks in the bundle, and inconsistent UI between adjacent panels | The architecture solves an organisational coupling problem the team did not have, and its costs land on users regardless | Consolidate to one build with strong module boundaries until deploy coupling is a measured, named bottleneck (Modular Monolith in Architecture). |
| Framework picked before the architecture | The team is arguing about rendering strategy inside a framework that is opinionated about a different one | The framework choice silently answered the architecture question, and now the product is fighting the answer | Name the architecture criteria first; most frameworks can serve several of the five shapes once you know which one you want (Choosing a Framework). |
| The decision was correct three years ago | Every new feature is disproportionately expensive and nobody can say why | The product moved from content to application, or from one team to five, and the architecture did not | Re-run the criteria annually. The output is usually one route moving, not a rewrite. |
How to build it
Most important first.
- Start from the content-versus-application question, because it dominates everything else. A screen whose value is its text wants HTML on the wire; a screen whose value is a manipulable model wants a long-lived client.
- Then ask about deployment independence, because it is the only criterion an architecture change can buy and a code change cannot. If four teams genuinely need to ship on four cadences, that is a real force (Micro Frontends).
- Then discoverability and shareability: whether a URL must render usefully without executing your JavaScript — for crawlers, link unfurls, and for the person whose script never finished loading.
- Then interactivity depth: how much state must survive a navigation. "The user is mid-edit" is a much stronger constraint than "the page has a dropdown".
- Then team shape, honestly. One team of six does not need runtime composition; it needs a modular monolith and a build (Modular Monolith in Architecture).
- Write the decision down with the criterion attached — "static, because every page is content and no team ships more than weekly". A recorded criterion is the thing that lets a future team notice when it stopped being true.
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- Architecture decides how much of your interface exists without JavaScript. Server-rendered and static HTML is available to assistive technology at first paint; a client-rendered shell is an empty landmark-free document until the bundle executes (Client-Side Rendering).
- Navigation ownership is an accessibility question. When the browser navigates, it resets focus to the document and announces the new page. When JavaScript navigates, nothing happens unless you make it happen, and a screen-reader user is left on a control that no longer exists (Focus Management).
- Micro frontends fragment the things assistive technology depends on being page-global: heading order, landmark structure, focus order and live-region announcements are all properties of the composed page, and no individual team owns the composed page (Micro Frontends).
- A slow architecture is an accessibility problem in its own right. Screen-reader users and switch users pay main-thread contention the same as everyone else, plus the cost of an accessibility tree that is being rebuilt underneath them (The Accessibility Tree).
What can go wrong
- Choosing by the loudest recent constraint. A team burned by a slow SPA rebuilds as an MPA and discovers their editor cannot keep state; a team burned by a rigid MPA rebuilds as a SPA and discovers the back button now needs a ticket.
- Choosing the architecture before the requirements exist, so the first genuinely hard screen has to fight it.
- Adopting a meta-framework and assuming that settles the question. It gives you every option per route, which means the decision still has to be made — now implicitly, one route at a time, by whoever is on call.
- Deciding correctly and then not re-deciding. The product that was five content pages three years ago is now an application, and the architecture nobody revisited is why every feature is expensive.
- The mitigation failing: writing a criterion down and then treating it as permanent. A criterion is a claim about the present, and it is supposed to expire.
- Every architecture with a client bundle has a version-skew race: a tab loaded before a deploy is calling an API that has moved on. The MPA has the smallest window because it re-fetches the document per navigation; the SPA has the largest because the tab may live for days (Long-Lived Clients and Version Skew).
- On composed pages, the order in which independently deployed units finish loading is not fixed, so anything that depends on two of them being present must express that dependency rather than assume load order.
- Where rendering happens decides where data can leak. Server-rendered HTML embeds whatever the server put in it — including, in real incidents, the whole user object rather than the three fields the page needed (Server-Side Rendering).
- A client-rendered application must assume every route it knows about is discoverable. Route definitions, feature flags and admin screen names are all in the bundle, and hiding a route is never authorization (Authorization-Aware UI).
- Micro frontends and third-party composition extend the trust boundary: any unit you compose at run time executes with the full authority of the page it lands in (Third-Party Scripts and the Supply Chain).
- A static site has the smallest server-side attack surface because there is no per-request server logic to attack — but it also has nowhere to enforce anything, so anything private needs a real backend behind it (The Browser Security Model).
- "SPAs are the modern choice." Modern is not a criterion. The genuinely modern position is that the decision is per-route and the browser gives you a great deal for free that you should think hard before rebuilding (MPA vs SPA).
- "Server rendering makes it fast." It changes what arrives first. It also adds a server on the critical path, and a hydration step that can leave a page looking ready while it is not (Hydration).
- "Micro frontends are how large companies do frontend." Some do; many large companies run a single well-modularised frontend deliberately, and the ones that adopted runtime composition did it to decouple teams, not to make anything faster (Micro Frontends).
- "This is a framework choice." The framework is downstream. You can build any of these five shapes with most of the major frameworks, and picking the framework first is picking the answer before the question (Choosing a Framework).
- "We can migrate later if we are wrong." You can, and it will cost more than the original build, because by then the architecture has leaked into every component's assumptions about where data comes from.
Measuring it, and what changes in the field
- Field data segmented by route type is the honest input: the loading and interaction profile of your content routes and your application routes are different products and should be read separately (Real User Monitoring).
- The Network panel on a cold cache tells you what a first-time visitor pays before anything is usable, which is the number an architecture decision actually moves (Reading a Network Waterfall).
- Deployment metrics are architecture metrics: how many teams are blocked per release, how long a change takes to reach production, how often a rollback affects unrelated features.
- Bundle composition by route answers "is this an application or a document viewer with an application bolted on" more honestly than any architecture diagram (Bundle Analysis).
- On a slow device, the gap between architectures is mostly script execution: parsing and compiling a large bundle is CPU work that scales with the device, and the client-heavy options pay it first (The Real Cost of JavaScript).
- On a high-latency network, the MPA's per-navigation round trip is the visible cost and the SPA's prefetched routes are the visible win — which is exactly the inverse of the cold-start comparison.
- With a large team, the deployment-independence criterion grows until it outweighs everything on this list. With a small team it is close to worthless.
- In a long-lived tab, the SPA accumulates: retained routes, stale caches, and an application version that no longer matches the API it is calling (Long-Lived Clients and Version Skew).
- Deciding per route rather than per product is the most defensible answer and the most expensive one to operate: two rendering models, two sets of failure modes, and a team that has to hold both.
- Every step toward the client buys interactivity and preserved state, and pays for it in first-load cost, memory retention and reimplementing browser behaviour. Every step toward the server buys cheap first loads and pays in round trips and lost client state.
- Recording the criteria costs a document that will be wrong in eighteen months. The alternative is an architecture nobody can explain, which is wrong immediately and silently.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALThe five shapes and the criteria that separate them describe where HTML comes from and who owns navigation, which is true regardless of language or framework — the same decision exists for a Rails application with server-driven updates, a React router, an Astro site and a Java template stack, and it lands in the same places.
- FRAMEWORK-SPECIFICMeta-frameworks such as Next.js, Nuxt, SvelteKit and Remix deliberately blur these boundaries per route, so "which architecture is this" can have a different answer on every URL; a plain Vite single-page build or a plain Django project has exactly one answer for the whole product, which makes the decision more visible but less adjustable.
- SIMPLIFIEDReal products are hybrids: a static marketing site in front of a server-rendered catalogue in front of a client-rendered account area is extremely common and is not a compromise. The five-way split is a way to name the options, not a claim that a product picks one.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Software Design — module boundaries, dependency direction and the coupling metrics that tell you whether a single build is genuinely holding teams back, or whether the problem is that nothing inside it has a boundary.
- — Distributed Systems — once frontend units deploy independently, the composed page is a distributed system with partial failure and version skew, and it inherits that field's problems whether or not anyone planned for them.