Skip to main content

On This Page

Draft / Scheduled Content

This article is a draft or scheduled for future publication. The content is subject to change.

The SPA Obsession Has Ruined the Web

6 min read
Share

The Promise vs. The Reality

Ten years ago, the frontend community made a collective decision. We looked at server-rendered HTML—the technology that powered the web’s first two decades—and decided it was obsolete. We wanted transition animations, desktop-like responsiveness, and instant page updates. We wanted Single Page Applications (SPAs).

We got what we wanted. And in doing so, we broke the web.

Today, visiting a simple content site, a blog, or an e-commerce store often involves downloading several megabytes of JavaScript before anything renders. We stare at loading spinners, only for the page layout to shift violently once the API requests finally resolve. The browser’s native back button is either hijacked or broken. Link sharing is a gamble on whether client-side routing works.

The promise was a faster, smoother user experience. The reality is a sluggish, fragile, and bloated web where developers spend 80% of their time managing client-side state synchronization, build toolchains, and hydrate-on-load hacks.

It’s time to admit it: the SPA obsession was a mistake for 90% of the web.

The Cost of Client-Side Rendering

When you build a pure client-side SPA, you shift the work of rendering the page from a high-performance server onto the user’s device.

Think about what happens when a user visits a typical React or Vue SPA:

  1. The browser requests index.html.
  2. The server responds with a blank HTML file containing a <div id="app"></div> and a couple of <script> tags.
  3. The browser downloads the JS bundles (often 500KB to 2MB uncompressed).
  4. The browser parses and executes the JS (blocking the main thread, especially on low-end mobile devices).
  5. The JS mounts the framework, renders the initial UI skeleton, and fires off three or four API requests.
  6. The server processes the API requests and sends back JSON.
  7. The JS processes the JSON, re-renders the DOM, and finally displays the content.

Compare this to the classic Multi-Page Application (MPA):

  1. The browser requests /page.
  2. The server queries the database, renders the HTML with the content baked in, and responds.
  3. The browser displays the page immediately.

By shifting rendering to the client, we have traded server CPU cycles for user battery life, bandwidth, and patience. On a top-of-the-line MacBook Pro, the client-side render takes a fraction of a second. On a $150 Android phone with a patchy 4G connection, it takes six seconds of blank screens and layout shifts.

We didn’t solve a user problem; we solved a developer preference.

The Complexity Spiral

To fix the performance issues we created by using client-side SPAs, we had to invent increasingly complex solutions.

First, we realized SEO was broken because search engine crawlers just saw a blank <div>. So we invented Server-Side Rendering (SSR). Now, the server renders the HTML, sends it to the browser, and then the browser downloads the JS to “hydrate” the page.

But hydration is slow and heavy. So we invented Static Site Generation (SSG) to pre-render the pages at build time.

But SSG doesn’t work for dynamic data. So we got Incremental Static Regeneration (ISR), which dynamically regenerates static pages in the background.

Then we decided hydration was still too heavy, so we invented Streaming SSR, Resumability (Qwik), and React Server Components (RSC).

Look at this diagram of a modern React Server Component flow:

sequenceDiagram
    participant Browser
    participant Server
    participant Database

    Browser->>Server: Request Page /dashboard
    Server->>Database: Query Server Component Data
    Database-->>Server: Return Data
    Server-->>Browser: Stream HTML + RSC Payload (chunks)
    Note over Browser: Render HTML skeleton
    Browser->>Server: Request Client Component JS Bundles
    Server-->>Browser: JS Bundles
    Note over Browser: Hydrate Client Components & Bind Event Listeners

We have rebuilt the entire classic backend rendering pipeline, but with ten times the abstraction, massive JavaScript bundle footprints, and a build system so complex that no single developer fully understands how their code gets from src/ to dist/.

We are running a server to render HTML, which sends a JSON description of the UI to a client, which runs a virtual DOM to compare it against the existing DOM, all to update a text node.

We are using a space shuttle to cross the street.

Where SPAs Actually Make Sense

Let’s be fair: SPAs aren’t useless. They are a highly effective architecture for a specific class of applications.

If you are building an interactive dashboard like Google Sheets, Figma, a music production suite, or a real-time chat application, you need an SPA. These are applications in the truest sense of the word. They maintain a complex local state, require immediate feedback loops, and aren’t crawled by search engines. The user logs in once and stays on the page for hours. The upfront download cost of the JS bundle is amortized over the long session.

But most websites are not Figma. They are content delivery channels.

  • An e-commerce store is a document system with a shopping cart.
  • A news site is a document system.
  • A blog is a document system.
  • A marketing site is a document system.

When you use an SPA framework (like Next.js, Nuxt, or SvelteKit) to build these, you are using the wrong tool. You are trading SEO, performance, and simplicity for the convenience of writing component-based UI.

The Return of the Server (and HTML)

Fortunately, the pendulum is swinging back. Developers are realizing that we don’t have to choose between bloated SPAs and static, boring 1990s web pages.

Modern tools allow us to build dynamic, interactive interfaces without the JavaScript tax:

  • Astro: Compiles your component-based code (React, Vue, Svelte) to zero-JS static HTML by default. It only ships JS for the specific parts of the page that actually require interactivity (the “islands” architecture).
  • HTMX: Extends HTML to allow you to make AJAX requests, trigger CSS transitions, and swap page fragments directly from server responses, completely bypassing the need for a client-side routing framework.
  • Hotwire (Turbo/Stimulus): Basecamp’s approach to sending HTML over the wire, allowing multi-page apps to feel like SPAs without the complex client-side state.

Here is how simple an interactive search can be with HTMX:

<!-- No React, no state store, no bundler, just pure HTML -->
<input type="text" name="q" 
       placeholder="Search products..." 
       hx-post="/search" 
       hx-trigger="keyup changed delay:500ms" 
       hx-target="#search-results">

<div id="search-results">
  <!-- Server renders and injects HTML directly here -->
</div>

The server does what it is good at: querying the database and generating HTML. The client does what it is good at: rendering HTML and handling user input.

Stop the Madness

The next time you start a new project, don’t default to npx create-next-app or whatever the framework of the month is.

Ask yourself: Is this an application, or is it a website?

If it’s a website, choose a server-driven framework. Keep your JavaScript footprint minimal. Trust the browser to handle navigation, history, and rendering. Your users, your cloud bill, and your mental health will thank you.

Related Content