Rendering

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.

User
Browser
Network
Server
JS Engine
Compositor
GPU
Display
  1. 01

    URL entry / link click

    01 User 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.

  2. 02

    Discard resources + HTTP request

    02 Browser 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.

  3. 03

    DNS / TCP / TLS

    03 Network 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.

  4. 04

    Server builds complete HTML

    04 Server 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.

  5. 05

    Transfer the complete HTML

    05 Network 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.

  6. 06

    HTML parse → DOM / CSSOM / Layout

    06 Browser 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.

  7. 07

    Raster + layer composite

    07 Compositor 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.

  8. 08

    GPU builds the frame

    08 GPU 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.

  9. 09

    Content on screen — FCP / LCP

    09 Display 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.

  10. FCP / LCP reached (no JS required)
  11. 10

    (Optional) JavaScript execution — Progressive Enhancement

    10 JS Engine (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.'

  12. TTI — interactive parts that don't need JS were already usable from paint
  13. 11

    Link click → back to ① (full navigation)

    11 User 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.
'MPA' is industry jargon — there is no formal, dedicated entry for it in MDN, W3C, or Wikipedia. This isn't because it's obsolete; it's because MPA is the default of the web — the baseline doesn't need a label. SPA has a Glossary entry because it's the named exception. For a technical definition, borrow from web.dev's 'Rendering on the Web': server-side rendering ('rendering an app on the server to send HTML, rather than JavaScript, to the client') and static rendering ('producing a separate HTML file for each URL ahead of time'). Wikipedia's SPA article refers to the same concept as 'traditional website' / 'classic page redraw model'. In practice, Wikipedia, Amazon, GitHub, news sites, e-commerce, and government sites still run on MPA, and since around 2023 modern MPA (Astro / Hotwire Turbo / HTMX / 37signals) has been getting visible re-evaluation amid 'SPA fatigue.'
Q. What's the difference between MPA and SSR? Both 'build HTML on the server', right?
They overlap enough to get conflated, but the framing differs. MPA is an architectural style — each URL returns its own independent HTML document, so navigation equals 'request a new document'. SSR is a rendering technique (the when) — the act of generating HTML on the server. Traditional MPAs almost always use SSR, but Next.js / Remix / SvelteKit combine SSR with client-side routing (SPA-like navigation without discarding the DOM). So MPA is 'a design that uses SSR', not 'equals SSR'.
Q. What exactly is 'modern MPA'?
An umbrella term for approaches that patch traditional MPA's weaknesses (white flash on navigation, resource re-fetch, lost UI state) using browser-standard APIs plus minimal JS. Examples: (1) Astro View Transitions: uses the browser's 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?
In an MPA, navigation means 'a new session', so state must be saved explicitly. Options: (1) URL query params (?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?
Astro takes a clear 'MPA by default + SPA (Islands) where necessary' stance. Each page is server-rendered full HTML (= MPA) by default. Only components tagged with 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?
On initial-load metrics (FCP / LCP), yes — structurally so. MPA returns complete HTML, so painting doesn't wait on JS, and crawlers don't either. TTI / INP (interactivity metrics) are very fast on JS-free MPA, and modern MPA with Turbo / Islands approaches them closely. But for continuous navigation within a session, SPA tends to feel faster — MPA requests a full HTML from the server every time, so SaaS-style UI where the same view morphs frequently favors SPA. In short: first-paint-wins → MPA; continuous-interaction-wins → SPA. Pick based on which axis matters.

Design Signals

Where MPA shines — and when it's time to graduate to SPA / SSR.

MPA fits here

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.

MPA fits here

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.

MPA fits here

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.

Time to leave MPA

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.

Time to leave MPA

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.

Time to leave MPA

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.