MPA (Multi-Page Application)
A Multi-Page Application (MPA) is the traditional web architecture where the server returns a complete HTML document for each URL. Most of the web (Wikipedia, Amazon, GitHub, news sites) still uses MPA. Modern MPA is being re-evaluated via the View Transitions API, Hotwire, Astro, and HTMX.
Metrics Primer: FCP / LCP / TTI
The swim-lane diagram below marks FCP / LCP / TTI milestones. Knowing what each metric actually measures upfront makes the diagram much easier to read.
- FCP First Contentful Paint — first visual content painted
- The moment text, images, SVG, or non-empty canvas is first painted on the browser viewport. A background-color-only screen doesn't count. A Core Web Vitals secondary metric — under 1.8 s is considered good.
- LCP Largest Contentful Paint — largest content painted
- When the largest content element (hero image, big heading, etc.) is painted. A proxy for 'meaningful first paint' and a primary Core Web Vitals metric. Under 2.5 s is good; over 4 s needs work.
- TTI Time to Interactive — when interactivity starts
- The moment the page is fully interactive — all event listeners attached and the main thread free of Long Tasks for 5 seconds. Dropped from the Core Web Vitals primary set (replaced by INP) in 2024, but still important as the 'feels usable' moment for real users.
Rendering Lifecycle
MPA's 11 phases. There is no SPA-style 'empty paint → JS → content paint' two-cycle — the full HTML is received and painted in one pass, with no hydration. The catch: clicking a link at phase 11 loops back to phase 02 (discard resources, new request). This round-trip per navigation is MPA's characteristic UX trade-off.
-
01 URL entry / link click
The user enters a URL or clicks a link. In an MPA, this signals a request for a new page — a new document entirely.
-
02 Discard resources + HTTP request
The browser discards the current page's DOM, JS state, and memory, then issues a new HTTP request. This 'hard reset' is MPA's defining trait — the opposite of SPA, which keeps state alive and patches the UI. Until the View Transitions API arrived, this reset was the UX cost of MPA.
-
03 DNS / TCP / TLS
DNS resolution → TCP handshake → TLS handshake at the network layer. Same-origin MPA navigation reuses TCP / TLS via Keep-Alive and benefits from DNS caching — the practical reason MPA navigation doesn't feel as slow as the theoretical round-trip suggests.
-
04 Server builds complete HTML
The server runs database queries, session / auth checks, and template engines (ERB for Rails, Django templates, Laravel Blade, etc.) to assemble the complete HTML. Per web.dev's taxonomy, server-side rendering is 'rendering an app on the server to send HTML, rather than JavaScript, to the client' — MPA is this pattern in its most traditional form.
-
05 Transfer the complete HTML
The server's complete HTML plus linked CSS / images is returned in the HTTP response. Unlike the SPA 'empty shell → JS bundle' two-stage model, MPA ships a finished document in a single pass.
-
06 HTML parse → DOM / CSSOM / Layout
The browser parses HTML into a DOM, CSS into a CSSOM, combines them into a render tree, and runs layout (geometric calculation). Since the MPA HTML already contains the full content, this phase is the real rendering preparation — not a scaffolding step.
-
07 Raster + layer composite
The compositor thread rasterizes tiles and builds composite commands from the layer tree. Unlike SPA's two-cycle pattern (empty paint first, content paint after JS), for MPA this is the first and only rendering cycle.
-
08 GPU builds the frame
The GPU runs composite commands and produces the frame buffer. Zero JavaScript has executed so far; the full HTML is enough to render complete content.
-
09 Content on screen — FCP / LCP
In sync with vsync, the frame is scanned out. The user sees real content for the first time. Since MPA doesn't wait on JS execution, FCP / LCP tend to be structurally fast — MPA's traditional advantage on Core Web Vitals initial-load metrics.
- FCP / LCP reached (no JS required)
-
10 (Optional) JavaScript execution — Progressive Enhancement
JavaScript may run to add interactivity (form validation, dropdowns, etc.). MPA's traditional philosophy is Progressive Enhancement — ship HTML that works without JS first, then layer JS on top. Modern MPA uses approaches like Astro Islands, Hotwire Turbo, and HTMX — 'MPA everywhere, dynamic only where needed.'
- TTI — interactive parts that don't need JS were already usable from paint
-
11 Link click → back to ① (full navigation)
Clicking a link to the next page restarts from phase 02 (resource discard). Each navigation resets the DOM and JS state — MPA's defining behavior. Modern MPA uses the View Transitions API or Turbo / Astro's specialized routing to soften this reset feel.
Frequently Asked Questions
Common questions that come up while learning MPA — 'Is MPA outdated?', 'How is it different from SSR?', etc.
Q. Is 'MPA' even a formal term? Neither MDN nor Wikipedia has a dedicated entry for it.
Q. What's the difference between MPA and SSR? Both 'build HTML on the server', right?
Q. What exactly is 'modern MPA'?
startViewTransition() API to animate page transitions, delivering an SPA-like feel while staying HTML-based. (2) Hotwire Turbo (Rails): intercepts navigation with fetch, applies only the DOM diff, and updates the URL — server-side is a normal MPA. (3) HTMX: fetches partial server updates via HTML attributes (hx-get / hx-post), building dynamic UI with almost no JS. (4) 37signals' ONCE: a line of RoR-based SaaS products built around MPA-first thinking. Common theme: 'Don't depend on the JS ecosystem — put the server and HTML at the center.'Q. How do you manage state — e.g., a form in progress — in an MPA?
?filter=active&page=3&sort=date): shareable, bookmarkable, back-button friendly — ideal for search, pagination, sorting. (2) Server session (cookie + server-side DB / Redis): login state, shopping carts, etc. (3) sessionStorage / localStorage: form auto-save, tab-specific UI state. (4) Hidden form fields: intermediate data in multi-step forms. Unlike SPA's 'keep everything in memory', MPA forces you to consciously choose where state lives — one of the reasons MPA is considered robust.Q. Is Astro a SPA or an MPA?
client:load / client:visible get a JS runtime (the Islands Architecture); the rest ships as pure static HTML. The View Transitions API smooths navigation, so the UX line with SPA blurs — but architecturally Astro lives on the MPA side. It's one of the modern answers to 'tired of SPA but don't want to compromise on UX.'Q. Does MPA have an SEO / Core Web Vitals advantage over SPA?
Design Signals
Where MPA shines — and when it's time to graduate to SPA / SSR.
Content-centric sites
Blogs, news, docs, e-commerce product pages, media sites. SEO / OGP / first-paint performance are top priority; users typically 'read one page, move to the next'. This is MPA's sweet spot. SSGs like Astro / Hugo / Jekyll are MPA along the same axis.
Minimize JS runtime cost
For mobile / low-end devices / regions with weak networks, MPA's 'works with almost no JS' property is a real advantage. With Progressive Enhancement, core features still work with JS disabled — 3G users and older devices see dramatically lower bounce rates.
The team has MPA-era assets
Teams using Rails / Django / Laravel / ASP.NET MVC are fastest building in-stack with MPA. Adding Hotwire (Rails) / HTMX brings a modern UX. Avoiding the 'build a dedicated JS team' cost that comes with SPA is a reason this path is being revisited.
UI changes frequently within one view
Drag-and-drop board editing, mind maps, collaborative editors, dynamic UI during a voice call, etc. Re-fetching the entire page for each in-view interaction is impractical. SPA or SPA + SSR (Next.js etc.) is the right fit.
Real-time updates are central
Slack-style chat, live dashboards, notification-heavy apps. You need WebSocket / Server-Sent Events to receive continuous push from the server and update just a portion of the UI rather than the whole page. MPA can do it, but SPA fits more naturally.
Offline operation required
PWAs leveraging Service Workers need UI that runs offline. You can combine MPA + Service Worker, but SPA — where UI logic is self-contained and doesn't depend on server responses — is easier. For mobile-app-like experiences, SPA is the cleaner path.
Common Pitfalls
UI state lost on navigation
A classic MPA complaint: user typing in a form navigates away, comes back, and everything is gone. Fixes: (a) warn via beforeunload, (b) auto-save to sessionStorage on each input, (c) save a draft to the server before navigating, (d) carry intermediate state in URL params. Hotwire Turbo or the View Transitions API keep the DOM alive across navigations, significantly reducing this pain.
White flash on navigation ruins the feel
The classic 'flash of white' during navigation. Fixes: (a) apply transition animations via CSS page-transition-* or the View Transitions API, (b) prefetch the next page with instant.page / Quicklink, (c) layer on Turbo / Astro client-side routing. Combining these can dramatically improve MPA's perceived speed.
CDN caching vs. personalization is hard to balance
Caching a personalized MPA at the CDN can leak one user's page to another. Fixes: (a) Cache-Control: private / Vary: Cookie, (b) pull login-specific parts (header username, etc.) asynchronously via CSR / Edge Functions, (c) use Edge Side Includes (ESI) to swap only personalized fragments at the edge. It's not as simple as SSG.
JS integration tends to go ad-hoc
Assuming 'MPA means barely any JS' tends to produce ad-hoc <script> tags and jQuery-style snippets scattered across pages, eroding maintainability. Fix: adopt an MPA-first JS integration pattern from the start — Hotwire / HTMX / Astro Islands — and agree as a team on where JS belongs.