Skip to content

    Frontend System Design

    From Multi-Page Sites to SPAs and Back: How Web App Architecture Evolved

    Follow web app architecture from multi-page sites through Ajax and single-page apps to SSR, hydration and islands, and learn why each step was a trade-off.

    By Published 8 min read

    Share

    Focused lessonbeginnerFrontend System Design

    Before this: Frontend System Design: The Big Picture

    What you'll learn: By the end of this lesson you will be able to describe the main steps in web app architecture (multi-page sites, Ajax, single-page apps, server rendering with hydration, and islands), name the problem each step solved and the new cost it created, and use two questions, where the HTML is made and who handles navigation, to place any app you meet.

    A restaurant can serve a full set menu each time you sit down, or bring one dish at a time to the same table. The first is simple. The second feels faster once you are seated, but the staff must now track the whole meal. Web architecture has swung between the two.

    This lesson is the map. Rendering Strategies treats CSR, SSR, SSG, ISR and streaming in depth, so each gets only a working definition here. The running example is a grocery storefront.

    Verified against MDN, web.dev, Hotwire Turbo, Astro and React documentation on 2026-11-08.

    The core idea

    Every architecture below answers two questions: where is the HTML produced, and who owns navigation, the browser or your JavaScript? Each step in the history changed one answer to fix a pain, and paid for it elsewhere. Three labelled illustrations follow: a minimal example (a hand-made router), a realistic example (the storefront in "In practice") and a failure example (a hydration mismatch).

    Step 1: the multi-page application

    In a multi-page application (MPA) every link or form submission asks the server for a complete new document, and the browser replaces the old page. MDN's View Transition API page uses exactly this contrast: transitions can animate "between DOM states in a single-page app" or "the navigation between documents in a multi-page app".

    Figure 1. A click in a multi-page app and in a single-page app.

    sequenceDiagram accTitle: One click in an MPA compared with one click in an SPA accDescr: In a multi-page app the click makes the browser request a whole new HTML document and replace the page. In a single-page app the click is handled by JavaScript, which fetches data and updates part of the current document. participant U as Shopper participant B as Browser participant S as Server alt Multi-page app U->>B: click a link B->>S: GET the next page S-->>B: complete HTML document Note over B: page replaced, scripts and state start over else Single-page app U->>B: click a link Note over B: JavaScript intercepts the click B->>S: fetch the data S-->>B: JSON Note over B: JavaScript updates part of the same document end

    How to read this: same click, two branches. The first returns a whole document; in the second the page stays alive and only data travels.

    An MPA is simple: the URL is the state, Back works, and each page can be cached or indexed as a document. The cost is that nothing survives a click, so even adding an item to a cart is a full navigation.

    Step 2: Ajax

    Ajax was the fix. MDN defines it as a technique where a web app "fetches content from the server by making asynchronous HTTP requests, and uses the new content to update the relevant parts of the page without requiring a full page load". It was first built on XMLHttpRequest, which "can be used to retrieve any type of data, not just XML" (MDN). A retrospective on the news site heise reports that Jesse James Garrett named the approach in a February 2005 essay, citing Gmail, Google Maps and Google Suggest. The original essay could not be opened for this lesson, so treat the date as reported, not primary.

    Ajax added partial updates to the MPA, and created a new pain: content changed while the address did not, so the URL stopped describing what the user saw.

    Step 3: the single-page application

    A single-page application (SPA) pushes Ajax to its limit. MDN defines it as a web app that "loads only a single web document, and then updates the body content of that single document via JavaScript APIs such as Fetch". The History API repaired the URL: pushState() adds a session-history entry, replaceState() updates the current one, and on Back the browser fires popstate with the saved state.

    Minimal example: a hand-made client router

    This sketch is illustrative and was not run here. showPage is whatever fetches data and swaps the content.

    document.addEventListener("click", async (event) => {
      const link = event.target.closest("a[data-spa]");
      if (!link) return;
      event.preventDefault();
      history.pushState({}, "", link.href); // URL must be same-origin
      await showPage(link.pathname);
    });
    window.addEventListener("popstate", () => showPage(location.pathname));

    You now own click interception, the address bar, Back, and everything else the browser did for free. MDN names the trade-offs: "SEO, more effort required to maintain state, implement navigation, and do meaningful performance monitoring".

    The other cost is the first load: a near-empty shell, then the script, then the data. web.dev's Rendering on the Web warns that for CSR "the amount of JavaScript required tends to grow as an application grows, which can impact a page's INP" (see Core Web Vitals).

    Figure 2. The SPA request chain.

    Client-side rendering: three round trips before real contentBrowser to Static host (1 shell, 2 script); Browser to API (3 data)BrowserBrowserStatic host: empty shell, JS bundleStatic hostempty shell, JSbundleAPI: JSONAPIJSON1 shell, 2 script3 data

    How to read this: the shopper sees content only after step 3 finishes.

    Step 4: server rendering and hydration

    To show content sooner and give crawlers real HTML, teams rendered the same components on a server. web.dev defines SSR as "rendering an app on the server to send HTML, rather than JavaScript, to the client". Hydration then runs client scripts "to add application state and interactivity to server-rendered HTML". In React, hydrateRoot attaches to server-produced HTML and takes over managing the DOM inside it.

    Figure 3. Server rendering, then hydration.

    sequenceDiagram accTitle: Server rendering followed by hydration accDescr: The server returns finished HTML which the browser paints. The browser then downloads the JavaScript bundle and hydrates, after which the page responds to clicks. Between painting and hydrating the page looks ready but is not interactive. participant B as Browser participant S as Server B->>S: GET /products/9001 S-->>B: finished HTML Note over B: content painted B->>S: GET the JavaScript bundle S-->>B: bundle Note over B: hydrate, then the page responds

    How to read this: content appears after the first reply; between the two notes the page looks ready but does not respond.

    This fixed the blank first screen and restored a document per URL. The cost is hydration: web.dev notes it can improve first paint but "may have a significant negative impact on TBT and INP", and pages can look ready before they respond. It also adds a rule: React expects the first client render to be "identical with the server-rendered content".

    Step 5: less JavaScript, and back to HTML

    The latest steps shrink what the browser must run.

    • Islands. Astro describes an island as "an enhanced UI component on an otherwise static page of HTML"; client JavaScript is removed unless you opt in per component.
    • Server Components. React documents components that render "ahead of time, before bundling"; the client sees only the output, and libraries used there stay out of the bundle. Stateful parts are marked "use client".
    • HTML over the wire. Hotwire Turbo intercepts same-domain links and forms and swaps in HTML the server returns: "there's no client-side router to maintain".
    • Better MPAs. The Speculation Rules API can prefetch or prerender likely next pages ("near-instant"), and cross-document view transitions, once "impossible" per MDN, animate between pages. MDN marks the first experimental and not Baseline; check support for both.

    Figure 4. A product page split into server HTML and islands.

    Product page: mostly HTML, two interactive islandsServer HTML to Image gallery (HTML + small script); Server HTML to Add to cart (HTML + small script)HYDRATED IN THE BROWSERServer HTML: text and priceServer HTMLtext and priceImage gallery: islandImage galleryislandAdd to cart: islandAdd to cartislandHTML + small scriptHTML + small script

    How to read this: only the two islands in the zone ship code and hydrate.

    Trade-offs at a glance

    Architecture HTML made Navigation owner Strength Cost
    MPA Server, per page Browser Simple, URL is state State lost on each click
    SPA (CSR) Browser Your JavaScript App-like feel Router and state to maintain, first-load JS
    SSR + hydration Server, then browser Either Early content, real HTML Hydration cost, mismatch bugs
    Islands, Server Components Server, plus small client parts Usually server or framework Less client JavaScript A framework boundary to learn
    HTML over the wire Server Browser plus a small library No router in your code Server round trip per interaction

    In practice: splitting a storefront and its order dashboard

    This is a realistic scenario, not a case history. It is a composite, it does not describe any real company, and every figure is illustrative.

    The situation

    A grocery storefront and a staff order dashboard share one client-rendered SPA. Product pages are slow on mid-range phones and search results are thin. The dashboard is a long-lived tool full of live state, and it works well.

    Starting architecture

    One bundle serves both audiences: every visitor downloads dashboard code, and every product page is an empty shell until its data arrives.

    Options considered

    Option Fixes Costs
    One SPA plus code splitting Smaller first download Still a blank shell per product
    Server-render everything and hydrate Real HTML on every route Hydration cost, even for the dashboard
    Split: server-rendered product pages with islands, SPA kept for the dashboard Each side gets the model it suits Two build targets, one design system to keep aligned

    The decision

    The split. Product pages render on the server and hydrate two islands (Figure 4). The dashboard stays an SPA behind sign-in, where SEO is irrelevant and state is the point. Moving between them is a normal link, a document load.

    Rollout and verification

    Move one category first. Compare field loading and interaction metrics with the old pages, confirm each product URL returns real HTML to a plain fetch, and check Back and deep links.

    The 2 a.m. failure

    Hydration warnings appear and interaction metrics worsen on some phones. The cause is a "recently viewed" strip inside a typeof window !== 'undefined' branch: the server draws nothing, the client's first render draws items, so the trees differ. React's documentation lists this pattern as a mismatch cause and warns that in the worst case event handlers attach to the wrong elements. First checks: read the development warning, diff server HTML against the first client render, and search render code for window, dates and random values. Mitigation: render the strip after mount, in an effect. Follow-up: a test that renders on the server and compares with the first client render.

    Architect's notes

    • Choose per route, not per company. A catalogue, a checkout and a dashboard rarely want the same model.
    • Count what an SPA takes over from the browser: scroll, focus, history, error pages.
    • Send less JavaScript before sending it faster (code splitting), and compare options with Trade-off Analysis.

    Common mistakes

    • Treating history as a ladder. MPAs are still shipped, and newer designs borrow from them.
    • Changing the DOM on navigation without managing focus and the page title.

    Key takeaways

    • Two questions place any web app: where the HTML is produced, and who handles navigation.
    • Ajax and SPAs fixed state loss and moved routing and state into your code.
    • SSR and hydration fixed the blank first screen at the price of hydration work and a match rule.
    • Islands, Server Components and HTML over the wire send less JavaScript by default.

    Practice exercises

    Exercise 1 (20 min). Pick three sites you use and say where each produces its HTML and who handles navigation.

    Exercise 2 (45 min). Extend the minimal router so Back restores scroll position, and list what an MPA gives free.

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments