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
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:
- The browser requests
index.html. - The server responds with a blank HTML file containing a
<div id="app"></div>and a couple of<script>tags. - The browser downloads the JS bundles (often 500KB to 2MB uncompressed).
- The browser parses and executes the JS (blocking the main thread, especially on low-end mobile devices).
- The JS mounts the framework, renders the initial UI skeleton, and fires off three or four API requests.
- The server processes the API requests and sends back JSON.
- The JS processes the JSON, re-renders the DOM, and finally displays the content.
Compare this to the classic Multi-Page Application (MPA):
- The browser requests
/page. - The server queries the database, renders the HTML with the content baked in, and responds.
- 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
The CSS-in-JS Fever Dream is Over
For years, frontend developers insisted that CSS should be written in JavaScript. We got dynamic styling at the cost of massive runtime overhead, bloated bundle sizes, and broken browser style injection. Now, the fever dream is over. Tailwind and modern CSS features have won.
Your Unit Tests Are Mocking You
Unit testing with mocked dependencies has become a software industry obsession. We write tests that verify our code behaves against mock assumptions, resulting in green test suites that pass while production crashes. It is time to embrace integration tests with real, lightweight dependencies.
Why Python is the Worst Language to Teach Beginners
Python has become the default first language in universities and bootcamps. But its magical syntax, lack of explicit types, hidden memory models, and chaotic package ecosystem teach beginners terrible habits and make transitioning to other languages unnecessarily painful.