SevyDevy

Guide · Frontend

SPA vs MPA: Choosing the Shape of Your Web App

26 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • SPA vs MPA is a choice about who draws the next screen. A multi-page application (MPA) gets a fresh page from the server on every click.
  • A single-page application (SPA) loads one page once, then redraws parts of it with JavaScript, the code that runs inside the browser. Its address bar changes without a new page load.
  • An MPA is simpler and easy for search engines to read. The browser gives it the back button, a loading bar and announcements from screen readers (tools that read pages aloud) without extra code.
  • An SPA downloads more code first, then feels quick across many clicks. In return, your code must redo the back button, scroll position and focus (which part of the page the keyboard is on). Short visits from search suit an MPA; long sessions after login suit an SPA.
  • Beginners can stop after the section on mixing both shapes. Engineers get the bugs we found by testing a hand-written router, the code that maps URLs to screens. They also get the Navigation API, a browser feature for handling page changes now in all major browsers, and Chrome 151's timing of screen changes inside an SPA.

What goes wrong when a web app picks the wrong shape?

Pick the multi-page shape for a busy logged-in tool, and every click reloads the screen and can wipe half-typed work. Pick the single-page shape for pages people find on search, and first visits get slower and harder for crawlers, the programs search engines use to read pages.

That is the whole SPA vs MPA question: which shape fits which part of your product. Most products have parts of both kinds.

Picture a common setup. A wedding-vendor booking site has about 2,400 vendor pages for venues, caterers, decorators and photographers. Most visitors land on one from a search engine, look at two or three pages and leave.

The second part is the planner. After logging in, a couple builds a shortlist, tracks a budget of, say, Rs 18 lakh (1.8 million rupees), and messages vendors. A typical couple stays about 30 minutes and clicks around 60 times. The setup is made up, but the problem is real.

Today every click on the site loads a whole new page. On the planner, that hurts. Moving one caterer into the shortlist reloads the screen, the budget sidebar blinks, and a half-typed message to a vendor is lost.

So the team asks a fair question: should the whole site become a single-page app? This guide answers it part by part, and shows you how to check each answer yourself.

SPA vs MPA: what is the difference between them?

An MPA loads a new HTML document from the server for every page, while an SPA loads one document once and then changes it with JavaScript. The real difference is who draws the next screen: the server or the browser.

Two words first. A web page starts as an HTML document, the text file that tells the browser what is on the page. JavaScript is the code that can change that page after it arrives.

Multi-page application. An MPA is a site where each URL has its own HTML document, made by the server. Clicking a link asks the server for the next document. Example: each vendor page, like the one for a banquet hall on Ferozepur Road in Ludhiana.

Single-page application. MDN, the main web documentation site run by Mozilla, describes an SPA as an app that loads only a single web document. JavaScript then updates that document whenever different content must be shown. Example: a planner where the Budget tab only swaps the middle panel.

Client-side routing. Routing means deciding which screen belongs to which URL. In an SPA, a small piece of JavaScript called a router does this inside the browser, instead of the server doing it.

Picture a postcard stand at a fair. You pull out one card, look at it and pull out the next. Each card is complete on its own, and you can hand a single card to a friend. That is an MPA: every page is a whole card from the server.

Now picture a kaleidoscope. Lifting it to your eye takes a moment, but after that every small twist gives a new view at once. That is an SPA: one slow load, then quick changes without fetching a new page.

Where the picture breaks: a kaleidoscope already holds all its glass, but an SPA still fetches fresh data on later clicks. What it skips is the page around that data. And unlike a kaleidoscope view, a good SPA screen has its own address you can share.

MPA vs SPA at a glance
MPASPA
Who draws the next screenThe server sends a whole new pageJavaScript in the browser redraws part of the page
What travels on a later clickThe full HTML pageUsually only data, often as JSON (a plain text format for data)
Back button and page addressHandled by the browserHandled by your router code
Search enginesRead the HTML directlyMust run JavaScript, unless the server sends the first screen
First visitUsually lighterUsually heavier, because the app code comes first
Long sessionsEvery click starts a fresh pageOne page stays open, so memory and state build up

How does a multi-page application work?

In a multi-page application, every link click makes the browser leave the current page and fetch a whole new HTML document from the server. The browser then reads it, loads its styles and draws it.

Follow one click on the vendor pages, from a couple tapping a caterer's name to the new page on screen. Every step after the first involves the network or the server.

One click in an MPA

Each click repeats the whole trip. The old page, and anything only in its memory, is thrown away.

Diagram connections:

  • Tap a caterer's name to Ask for the new page
  • Ask for the new page to Build the full HTML
  • Build the full HTML to Send the whole page
  • Send the whole page to Draw it from scratch
  • Tap a caterer's name: Couple → Browser
  • Ask for the new page: Browser → Page server
  • Build the full HTML: Page server → Database
  • Send the whole page: Page server → Browser
  • Draw it from scratch: Browser → Screen

Look at how little the team has to write for this. On a full page load, the browser does a lot of work by itself:

  • The back and forward buttons work, because each page is a real entry in the browser's history.
  • Every page has its own URL, so a couple can send one decorator's page to a cousin.
  • The browser shows a loading indicator, so people know something is happening.
  • Screen readers, tools that read a page aloud for blind and low-vision users, announce the new page's title when it loads.
  • Each click starts a fresh document, so a memory leak (memory a page keeps but never frees) cannot pile up over hours of clicking.

Going back can even be instant. Browsers keep a back/forward cache, or bfcache: a snapshot of the whole page in memory, including its JavaScript state. The web.dev guide lists Chrome since version 96, Firefox and Safari.

That cache is easy to lose by accident. The web.dev guide, updated 16 October 2026, says Firefox on desktop will not cache a page with an unload event listener, code set to run as the page closes. It adds that Chrome has deprecated the unload event, so it no longer fires there except on iOS. The guide also says the Cache-Control: no-store header, a server instruction not to keep a copy of the page, restricts which pages can use the cache.

You can check a vendor page yourself. In Chrome, open DevTools (the browser's built-in developer tools), go to Application, then Back-forward Cache, and click Run Test. On success the panel reports Restored from back-forward cache; otherwise it lists the reasons.

The cost shows up on busy screens. Every click downloads and redraws the header, menu and sidebar again, even when only one number changed. Anything that lived only in the page's memory, like a half-typed message, is gone.

How does a single-page application work?

A single-page application loads one HTML document and a bundle of JavaScript once, then handles later clicks itself. It fetches only the data it needs and redraws just the part of the screen that changed.

The trick is the History API, a browser feature for changing the address bar from code. Calling history.pushState adds an entry to the browser's history and shows a new URL. MDN notes that the browser does not load that URL.

The new URL must have the same origin, meaning the same scheme (http or https), host and port, or pushState throws an error. When someone presses back or forward, the browser fires a popstate event, and your router draws the matching screen.

MDN adds that calling pushState itself does not fire popstate. So the router must draw the new screen when it pushes the URL, and again whenever popstate arrives.

Here is the Budget tab in an SPA version of the planner. Notice that the server sends the app only once, in the first box. After that, only data travels.

Opening the Budget tab in an SPA

The slow step happens once. Later clicks move a small piece of data and redraw one panel.

Diagram connections:

  • Load the app once to Tap the Budget tab
  • Tap the Budget tab to Change the URL
  • Change the URL to Fetch budget data
  • Fetch budget data to Redraw the middle panel
  • Load the app once: Browser → Page server
  • Tap the Budget tab: Couple → Router
  • Change the URL: Router → History API
  • Fetch budget data: Router → JSON API
  • Redraw the middle panel: Router → Screen

The first box is the slow one. The browser must download, read and run the whole bundle before anything useful appears. Later boxes are quick, because only JSON data crosses the network.

A small router in TypeScript

Here is the core of a client-side router in TypeScript, which is JavaScript with type labels. renderShortlist, renderBudget and renderNotFound are made-up helpers. Each one fetches its data and returns the finished part of the screen. If you do not write code yet, skip to the next heading.

typescript
const routes: Record<string, () => Promise<Node>> = {
  "/planner": renderShortlist,
  "/planner/budget": renderBudget,
};
let latest = 0;

async function show(path: string): Promise<void> {
  const ticket = ++latest;
  const render = routes[path] ?? renderNotFound;
  const view = await render();
  if (ticket !== latest) return; // a newer click already won
  document.querySelector("#app")!.replaceChildren(view);
}

document.addEventListener("click", (e: MouseEvent) => {
  if (e.defaultPrevented || e.button !== 0) return;
  if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
  const link = (e.target as Element).closest("a[data-spa]");
  if (!(link instanceof HTMLAnchorElement)) return;
  if (link.origin !== location.origin) return;
  if (link.hasAttribute("target") || link.hasAttribute("download")) return;
  e.preventDefault();
  history.pushState({}, "", link.href);
  void show(link.pathname);
});

window.addEventListener("popstate", () => void show(location.pathname));
void show(location.pathname);

What just happened? The two early returns leave a click alone when another script already handled it, or when a modifier key is held. That keeps Ctrl-click opening a new tab, as people expect from any link.

Links to other sites, links that open in a new tab or window, and download links are also left to the browser. For the rest, preventDefault stops the normal page load and pushState changes the URL instead. The latest counter makes an older, slower screen give up if a newer click has already won.

The links stay real links with real addresses. If the JavaScript fails to load, the link still opens its address as a normal page load. The person sees something useful only if the server sends real HTML for that screen; a bare app shell stays blank. The engineer section below shows what goes wrong when these checks are missing.

You can check this with curl, a command-line tool that requests a URL. This command prints only the HTTP status code the server returns for a deep planner URL.

bash
curl -s -o /dev/null -w "%{http_code}\n" https://your-site.example/planner/budget

Expect 200, meaning the server sent the app. If it prints 404, the server has no fallback, and every refresh on that screen breaks. On our small test server, the setup with a fallback printed 200 and the setup without one printed 404.

Which is faster, an SPA or an MPA?

Neither is faster in general: an MPA usually wins the first page, and an SPA usually wins the tenth click. Which one matters depends on how many screens a typical visit has.

Here is a test build of both shapes on a mid-range phone over 4G. The numbers are made up, so time your own build the same way before you decide.

Test build of the booking site on a mid-range phone over 4G (made up)
MPA vendor pagesSPA planner
First visit downloadAbout 70 KB of HTML and CSS (the code that styles a page)About 650 KB, mostly JavaScript
First screen readyAbout 1.2 secondsAbout 3.1 seconds
Each later clickAbout 0.9 seconds, whole pageAbout 0.2 seconds, one panel
Typical visit3 pages, arriving from search60 clicks, after login
Who handles back, scroll and focusThe browserYour code

Pause and predict: a searcher views three pages, and a couple makes 60 clicks. Which shape wins each visit, and on which screen does the SPA catch up?

Now do the sums: the first screen, plus each later click. Three vendor pages take about 3.0 seconds in total with the MPA and about 3.5 seconds with the SPA, mostly spent on its first load.

For a couple making 60 clicks, it flips. The MPA keeps them waiting about 54 seconds across the session, while the SPA needs about 15. The SPA first pulls ahead on the fourth screen: 3.7 seconds against 3.9.

So the length of the visit decides. A slow start only pays for itself when enough quick clicks follow it.

Both shapes have ways to close the gap:

  • An MPA can prefetch, which means loading the next likely page in the background before the click.
  • An MPA can animate between pages. Cross-document view transitions let two same-origin pages animate into each other, once both pages opt in with a short CSS rule.
  • MDN marks cross-document view transitions as not Baseline, meaning not yet in all major browsers. Treat the animation as a bonus, never a requirement.
  • An SPA can split its code: send only what the first screen needs, and load other screens when someone opens them.
  • An SPA can have its first screen made on the server, then let JavaScript take over. That is server-side rendering.

Server-side and client-side rendering are a whole topic of their own. This guide stays with the app's shape. The rendering patterns guide covers how the first screen gets drawn.

Are single-page applications bad for SEO?

Single-page applications can be found on search, but they make SEO (search engine optimisation, the work of getting found on search) harder. MDN lists SEO among the trade-offs of SPAs.

The reason is simple. A plain SPA sends a nearly empty HTML document, and the vendor's name, prices and reviews appear only after JavaScript runs. A crawler, the program that reads pages for a search engine, must run that code to see anything.

Google's JavaScript SEO guide says it crawls, renders and then indexes such pages. A page may wait in its render queue for a few seconds or longer. The same guide warns that not all bots can run JavaScript.

You can see what a crawler that skips JavaScript sees. This command fetches the raw HTML of one vendor page and counts the lines that contain the vendor's name. Put one of your own vendor addresses and names in place of VENDOR-SLUG and VENDOR NAME.

bash
curl -s https://your-site.example/vendors/VENDOR-SLUG | grep -c "VENDOR NAME"

A server-made page prints a number above 0. A plain SPA shell prints 0, because the name arrives only after JavaScript runs. On our test server, the vendor page printed 1 and the planner shell printed 0.

For the booking site this settles the vendor pages. Most new couples arrive from search, so those pages must arrive as complete HTML. The planner sits behind a login that crawlers never pass, so SEO does not matter there.

What does an SPA have to handle that an MPA gets for free?

An SPA takes over jobs the browser normally does on a page load, so your code must redo them: history, scroll, focus, titles, memory and errors. Each one is small, but forgetting any of them makes the app feel broken.

  • Back button. Every screen change needs pushState, and a popstate listener must redraw. Miss it, and back jumps out of the app entirely.
  • Scroll position. A new screen should start at the top, and going back should return people to where they were.
  • Focus and titles. On a full load, screen readers announce the new page. In an SPA, nothing is announced unless your code moves focus or announces the change.
  • Memory. A tab may stay open for hours. Timers and listeners that are never removed slowly eat memory, and the app gets sluggish.
  • Errors. React is a popular JavaScript library for building screens. Its docs say an error while drawing removes the whole app, unless an error boundary, a component that shows fallback content instead, catches it. You also need error monitoring in the browser.
  • State. A filter, a draft or a selected vendor now lives in memory across screens. MDN lists the extra effort to maintain state among the trade-offs.

Measuring speed gets harder too. Core Web Vitals are Google's three metrics for loading speed, responsiveness and visual stability. The web.dev FAQ on SPAs says each one was historically measured against the current full page load.

Route changes inside an SPA, called soft navigations, were not measured as new pages. The FAQ, updated 13 October 2026, says Chrome 151 added APIs (features your code can call) that measure Core Web Vitals across these route changes.

As of 19 October 2026, browser engines other than Chromium, the engine behind Chrome and Edge, do not support those APIs. Chrome now publishes experimental soft navigation metrics in CrUX, its public report of real-user data, and aims to drop the experimental label in May 2027.

The team adds one small function for the router to call after drawing each screen. It sets the title, moves focus and starts new screens at the top. The router passes true when the change came from back or forward.

typescript
function afterRouteChange(title: string, isBackOrForward: boolean): void {
  document.title = `${title} | Planner`;
  if (!isBackOrForward) window.scrollTo(0, 0);
  const heading = document.querySelector<HTMLElement>("#app h1");
  if (heading) {
    heading.tabIndex = -1;
    heading.focus({ preventScroll: true });
  }
}

Setting tabIndex to -1 lets code move focus to the heading without adding it to the Tab key order. Screen readers usually read the focused heading aloud, which tells the person the screen changed.

On back and forward it skips the jump to the top. MDN says that with history.scrollRestoration set to auto, the browser restores where the person had scrolled. MDN also says focus() scrolls the element into view by default, so preventScroll: true keeps that restored position.

In Gatsby's 2019 user testing with people with disabilities, focusing a heading worked best of the options tried. Announcements still varied between browser and screen reader pairs. The post's revised advice moves focus to a skip link, a link that jumps to another part of the page, on each route change. It also names the new page in a live region, a part of the page screen readers read out when it changes.

When should you choose an SPA over an MPA?

Choose an SPA when people stay long, click often and keep work on screen, like an editor or a dashboard. Choose an MPA when visits are short, pages must be found on search, or the team is small.

These signs point to an MPA:

  • Most visitors arrive from search or shared links and leave after a few pages.
  • The pages are mostly content to read, such as listings, articles or product pages.
  • Many users are on slow phones or patchy networks, where a big first download hurts.
  • The team is small and would rather let the browser handle history, focus and errors.

These signs point to an SPA:

  • People log in and stay for many minutes, clicking dozens of times.
  • Screens share a lot of live state, like a budget total that changes as you shortlist.
  • The screen must never blink or lose a half-typed draft between views.
  • The content sits behind a login, so search engines never need to read it.

The booking site hits the MPA list for its vendor pages and the SPA list for its planner. So the answer to the team's question is no, not the whole site.

Can you mix SPA and MPA in one web app?

Yes: public, content-heavy pages can be served as an MPA while one logged-in area runs as an SPA. Each area gets the shape that suits its job.

That is what the team builds. Vendor pages stay server-made HTML, cached on a CDN. The planner, behind the login, becomes an SPA with its own set of URLs.

In the diagram, look at the two entry points. Searchers never download the planner's code, and couples download it once when they open the planner.

The booking site's mixed setup

Public pages are postcards from the server. The planner is one kaleidoscope, loaded once after login.

Diagram connections:

  • Couples from search to CDN (Open a vendor page)
  • CDN to Page server (Cache miss)
  • Page server to Vendor and booking data
  • Logged-in couples to Planner SPA (Load once)
  • Planner SPA to JSON API (Fetch JSON)
  • JSON API to Vendor and booking data
  • Couples from search: Short visits to vendor pages
  • CDN: Caches vendor pages near users
  • Page server: Builds full HTML for each vendor
  • Logged-in couples: Long planner sessions
  • Planner SPA: Loaded once, then routes in the browser
  • JSON API: Shortlist, budget and messages
  • Vendor and booking data: One source for both halves

The bridge between the halves is a plain link. Moving from a vendor page into the planner is one full page load, and nobody minds one. After that, the planner does not reload.

The couple's half-typed message now survives a shortlist change, because the page stays open. The vendor pages keep their search traffic, because they still arrive as complete HTML.

There is a lighter version of the same idea. An island is a small interactive widget inside an otherwise plain server-made page. A date picker on a venue page, checking free wedding dates, is a good example.

A 2020 post on jasonformat.com describes this islands architecture. Its author credits the name component islands to Katie Sylor-Miller, then Etsy's frontend architect, from a 2019 meeting.

Beginners can stop here. The rest is for engineers who will build or review an SPA: the bugs a small router hides, the newer Navigation API, and browser support as of 19 October 2026.

Which bugs hide in a hand-written SPA router?

A short router, like the one this guide used to show, has at least three. It takes over clicks the browser should handle, drops the query string and hash, and can show the wrong screen after two quick clicks. The query string and hash are the parts of a URL after the question mark and the # sign.

That shorter router skipped the defaultPrevented, Alt key, origin, target and download checks, pushed link.pathname instead of link.href, and drew each screen as soon as its data arrived.

We type-checked both routers with TypeScript 7.0.2 in strict mode. Then we ran them in jsdom 30.1.3, a browser simulation for Node.js, which runs JavaScript outside the browser, with fake screens that we could slow down on purpose.

What each router did in our jsdom tests (19 October 2026)
TestShorter routerRouter in this guide
Ctrl-click a planner linkLeft to the browserLeft to the browser
Alt-click a planner linkTaken over by the routerLeft to the browser
Link to /planner/budget?view=month#topURL became /planner/budgetQuery and hash kept
Slow Budget click, then fast Shortlist clickURL /planner, screen showed BudgetURL and screen both Shortlist
A data-spa link to another siteStayed in the app, at that link's pathLeft to the browser
A data-spa link with target=_blank or downloadTaken over by the routerLeft to the browser

What just happened? The race is the nastiest bug. The router waits for each screen's data before drawing it, so a slow first answer can land after a fast second one and overwrite it.

The latest counter fixes the order: each call to show takes a ticket, and only the newest ticket may draw. A fuller router also cancels the old request with an AbortController, the browser's tool for stopping a request. It also catches a failed fetch and shows an error screen, and treats /planner/ and /planner as the same path.

The origin check matters for the opposite reason. Without it, a link to another site that wrongly carries data-spa keeps the person inside your app, on whatever page matches that path.

What does the Navigation API change for SPA routing in 2026?

The Navigation API fires one navigate event whenever any type of navigation starts, so a router does not need its own click listener. MDN calls it a successor to the History API, aimed at single-page apps.

Inside that event, a router calls intercept() to handle a same-document navigation with its own code. MDN's example skips navigations that should not be intercepted, such as cross-origin ones or downloads.

The defaults help too. After your handler finishes, the browser focuses the first element with the autofocus attribute, or the page body if none has it. On back and forward, it restores the scroll position.

That covers part of what afterRouteChange did by hand. Focusing the body is not the same as focusing the new heading, so you may still want to move focus to the heading yourself.

MDN marks the Navigation API Baseline since January 2026, meaning it works in the latest versions of all major browsers. If you support older versions than those, keep a History API fallback.

How do you explain SPA vs MPA in a system design interview?

Treat it as a decision for each part of the product, based on how people arrive and how long they stay, and name the costs of each shape out loud.

  1. Ask how people arrive and stay

    Do they come from search for a few pages, or log in and click for half an hour? This one answer drives most of the design.

  2. Name what must be found on search

    Public listings and articles need complete HTML from the server. Screens behind a login do not.

  3. Pick a shape for each area

    Say MPA for content pages and SPA for the workspace, or one shape if the whole product is one kind.

  4. List the SPA's extra duties

    Mention history, scroll, focus, titles, memory, error handling and old tabs after a deploy. It shows you know the price of the smooth feel.

  5. Show how to close the gaps

    Prefetching and view transitions for the MPA side. Code splitting and server rendering for the SPA side.

  6. Say how you would measure

    Time first loads and later clicks on cheap phones. Note that soft navigation metrics exist only in Chromium-based browsers so far.

Questions people ask

Is an SPA the same as client-side rendering?
Not exactly. An SPA is about the app's shape: one document that changes as you move between screens. Client-side rendering is about where a screen is drawn: in the browser. Most SPAs render in the browser, but an SPA can also get its first screen from the server and then take over.
Is an SPA always faster than an MPA?
No. An SPA usually has a slower first load, because it downloads more JavaScript before anything useful appears. It wins on later clicks, since only data travels. For short visits from search, a light MPA is often faster overall. Time both on real phones before deciding.
Can a single-page application have different URLs?
Yes. A good SPA gives every screen its own URL using the History API or the newer Navigation API, so people can bookmark, share and refresh any screen. The router reads the URL and draws the matching screen. The server must also answer each of those URLs with the app.
Why does my SPA show a 404 error when I refresh a page?
Because the browser asks the server for that exact URL, and the server has no file at that path. The router only runs after the app loads. Configure the server to return the app's main HTML document for every app URL, then let the router pick the screen.
What is a soft navigation?
A soft navigation is a screen change inside an SPA, where the URL changes but no new document loads. web.dev describes it as a user interaction that leads to both a paint and a URL change. As of 19 October 2026, only Chromium-based browsers from version 151, such as Chrome and Edge, can measure it.
Do I still need the History API now that the Navigation API exists?
Mainly for older browsers. As of 19 October 2026, the Navigation API works in the latest Chrome, Edge, Firefox and Safari. Its intercept() method also restores scroll and resets focus by default. If your users run versions older than Chrome or Edge 105, Firefox 147 or Safari 26.2, keep a History API fallback.
Should a small team start with an SPA or an MPA?
Usually an MPA, unless the core of the product is a long, interactive workspace. An MPA lets the browser handle history, focus and errors, so there is less to build and fix. You can add an interactive island or a small SPA area later, where it clearly helps.
Is a page with a few interactive widgets an SPA?
No. A server-made page with a few interactive parts, like a date picker or a photo carousel, is still an MPA page. Those parts are often called islands. It becomes an SPA only when JavaScript takes over moving between screens.

Sources and further reading

Practise this with Lala

Practise