What's a Single-Page App?


The web development community talks a lot about single-page apps, but are we all on a single page?

Heydon Pickering tackled this question in his similarly-named article What Is A Single-Page Application? What Is A Single-page Application? A quick explainer about single-page applications are what they are good for. heydonworks.com/article/what-is-a-single-page-application/ The TL;DR — spoiler alert! — is that it’s a website that uses a ton of JavaScript to improve user experience by showing you a loading spinner.

That’s obviously tongue-in-cheek, but it’s a reaction to the working definition that most people use. For better or worse, “single-page app” is usually a euphemism for “JavaScript framework app”.

I recently wrote about building a single-page app with htmx Building a Single-Page App with htmx | jakelazaroff.com People talk about htmx as though it's saving the web from single-page apps. Well, I guess I missed the memo, because I used htmx to build a single-page app. jakelazaroff.com/words/building-a-single-page-app-with-htmx/ using service workers to render everything client-side — no loading spinners in sight! In response, Thomas Broyer objected to the premise that htmx and single-page apps were opposites. He showed me an article that he wrote called Naming things is hard, SPA edition Naming things is hard, SPA edition What is a single-page application (SPA) exactly? How does it relate to client-side rendering (CSR)? (spoiler: SPA doesn't necessarily imply CSR.) blog.ltgt.net/naming-things-is-hard-spa-edition/ (which you should also go read!) that breaks down rendering into a spectrum.

Schema of SSR, ESR, SWSR and CSR, with grouping representing SSR-in-the-broader-sense (SSR and ESR) vs. BSR (SWSR and CSR), and which generate HTML (SSR, ESR and SWSR) or manipulate the DOM (CSR)

The rendering spectrum, by Thomas Broyer.

In a bid to cement my burgeoning reputation as a Quadrant Chart Guy The Website vs. Web App Dichotomy Doesn't Exist | jakelazaroff.com A one-dimensional spectrum can't sufficiently capture the tradeoffs involved in web development. jakelazaroff.com/words/the-website-vs-web-app-dichotomy-doesnt-exist/ , I feel compelled to add even more nuance to the situation:

A graph with two axes that intersect in the exact center, labeled SSR/CSR horizontally and SPA/MPA vertically.

I’m sorry. Kinda.

Okay, let’s define the extrema of each axis:

  • Server-side rendering (SSR) is when HTML is produced on a server and sent to the browser.
  • Client-side rendering (CSR) is when HTML (or some other representation, such as the result of a JSX transform) is produced on the client and applied to the DOM.
  • A multi-page app (MPA) is when a hyperlink click or form submission results in the browser replacing the current page with an entirely new document.
  • A single-page app (SPA) is when the browser never replaces the page with a new document, and instead makes all changes through client-side DOM manipulation.

If you just came here for an answer to the title, that’s it; I guess you can go home now. But I think it’s interesting to look at the various tools people use and how they fit in.

Most tools for building websites don’t lock you into just one quadrant. After all, any tool lets you drop in a plain un-enhanced <a> tag and at the very least get MPA behavior, and most JavaScript usage outside of Google Tag Manager relies on client-side rendering (even if done manually).

So: without casting any aspersions, here’s my ontology of web app architectures organized by rendering and navigation.

Traditional Web Frameworks and Static Site Generators

This is a pretty large tent, encompassing WordPress, Django, Rails (pre-Turbolinks) Jekyll, Hugo, Eleventy and myriad others. It also includes hand-authored HTML, though I wouldn’t describe that as a “tool” so much as a “way of life”.

Tools in this category are on the bottom left of the chart: server-side rendered multi-page apps.

A graph with two axes that intersect in the exact center, labeled SSR/CSR horizontally and SPA/MPA vertically. A shaded region labeled “Traditional Frameworks & Static Site Generators” covers the quadrant where SSR and SPA intersect.

The tradeoffs of this quadrant are well known:

  • The browser takes care of a lot of important accesibility features, such as letting screen readers know when the user navigates to a new page.
  • Delivering HTML first allows the content to be visible even if CSS or JavaScript fail to load.
  • Pages can load even faster if HTML is streamed in, rather than delivered all at once.
  • The full page must be downloaded and replaced on each navigation.
  • In fact, every interaction requires a network round trip.

This experience has remained mostly unchanged for 30 years. And it’s great! With only a little bit of HTML and CSS, you can make a pretty good website; the many Motherfucking Website motherfuckingwebsite.com Motherfucking Better Motherfucking Website bettermotherfuckingwebsite.com Website Even Better Motherfucking Website It's even more fucking perfect than the others motherfucking websites. evenbettermotherfucking.website variations Perfect Motherfucking Website 🖕 And it’s really more fucking perfect than the last guy’s. perfectmotherfuckingwebsite.com show just how far a few tags and properties get you. The low barrier to entry is one of the main reasons the web flourished.

Three decades on, improvements in HTML and CSS are starting to mitigate some of the downsides. Preloading resources, for example, allows the browser to preemptively download associated files, which can make navigation almost instantaneous. And cross-document view transitions — not yet well supported, but hopefully soon! — promise to allow multi-page apps to navigate with fancy animations.

That said: requiring a network request and a whole new page for every interaction is a pretty strong constraint! As developers’ ambitions grew, they leaned more and more heavily on JavaScript, which led to…

JavaScript Frameworks

Although JavaScript was invented way back in 1995, I don’t think a schism truly happened until 2010 or so. That’s when the stereotypical single-page apps began to emerge: rather than using small snippets of JavaScript to add client-side functionality to server-side rendered HTML, people started building apps with a JavaScript framework and rendering them on the client.

Note that I’m not talking about Next.js or similar tools (I’ll get to them in the next section). I’m talking about Backbone, Angular 1, React with a custom Webpack setup… basically, JavaScript apps before circa 2018, when people would ship an HTML file with an empty <body> except for one lonely <script> tag.

Used thusly, JavaScript frameworks are the diametric opposite of traditional web frameworks: both navigation and rendering happens on the client. As such, they fit neatly into the top right quadrant: client-side rendered single-page apps.

A graph with two axes that intersect in the exact center, labeled SSR/CSR horizontally and SPA/MPA vertically. A shaded region labeled “JavaScript Frameworks” covers the quadrant where CSR and SPA intersect.

What are the benefits of this quadrant?

  • The initial page load can be much faster once the JavaScript bundle is cached.
  • Page navigations can be instantaneous, since all the relevant state is already on the client.
  • Elements can persist across navigations, enabling e.g. uninterrupted media playback and fancy transition animations.
  • Modifying the UI without first going through the network enables much richer client-side interactions.

In practice, I think many of the purported benefits of client-side rendered SPAs turned out to be wishful thinking:

  • When bundles are cached with a hash of the full app code, every deploy busts the cache and forces the user to download the whole bundle again.
  • Page navigations tend to wait for API responses from a server and database in the same datacenter that would have served the HTML anyway.
  • Aspirations of being richly interactive are often fantasy; most websites are really just gussied up forms.

There are also more general drawbacks:

  • The client needs to download 100% of the UI code.
  • The initial page load (before the JavScript bundle is cached) will always be slower.
  • Page navigations are not accessible by default.
  • It’s way more difficult for apps here to be indexed by search engines.

If I sound critical of this category, it’s only because the industry has largely recognized these drawbacks and moved on to other architectures. While JavaScript frameworks are more popular than ever, they tend to exist as components of larger systems rather than than as app frameworks in and of themselves.

Client-side rendered SPAs still have their uses, though. When I made my local-first trip planning app A Local-First Case Study | jakelazaroff.com How I built a local-first app for planning trips, and what I learned about the current state of the local-first ecosystem along the way. jakelazaroff.com/words/a-local-first-case-study/ , I built it as a client-side rendered SPA. There was really no other way to build it — since the client has the canonical copy of the data, there’s not even a server to do any rendering! As local-first picks up steam, I hope and expect to see this architecture make a resurgence in a way that does capture the upside of the quadrant’s tradeoffs.

JavaScript Metaframeworks

JavaScript frameworks had about half a decade of client-side rendering glory before people realized that delivering entire applications that way was bad for performance. To address that, developers starting building metaframeworks1 — Next.js, Remix, SvelteKit, Nuxt and Solid Start, among others — that rendered on the server as well.

In metaframeworks, rendering happens in two different ways:

  1. When the user requests a page, the app runs on the server, rendering the appropriate HTML and serving it to the browser. This step is server-side rendered.
  2. Next, the browser requests the JavaScript bundle. That same app then runs in the browser, “hydrating” the already-rendered HTML and taking over any further interactions. This step is client-side rendered.

These steps slot neatly into the top left and top right quadrants, respectively:

A graph with two axes that intersect in the exact center, labeled SSR/CSR horizontally and SPA/MPA vertically. A shaded region labeled “JavaScript Metaframeworks” covers the SPA half, covering both SSR and CSR.

JavaScript metaframeworks are an attempt to get the “best of both worlds” between server-side rendered multi-page apps and client-side rendered single-page apps. In particular, they fix the cold cache initial page load and SEO drawbacks of the latter. With React Server Components, React-based metaframeworks can omit UI code from the JavaScript bundle as well.2

Depending on whom you ask, this is either good because it really is a “best of both worlds” situation, or bad because your UI is probably useless before it hydrates with the JavaScript (that your users still need to download). But “probably” in that sentence is doing at least some amount of lifting; many metaframeworks like SvelteKit and Remix embrace progressive enhancement and work without JavaScript by default.

A couple years ago, Nolan Lawson attempted to bridge the two camps SPAs: theory versus practice I’ve been thinking a lot recently about Single-Page Apps (SPAs) and Multi-Page Apps (MPAs). I’ve been thinking about how MPAs have improved over the years, and where SPAs still have an … nolanlawson.com/2022/06/27/spas-theory-versus-practice/ :

At the risk of grossly oversimplifying things, I propose that the core of the debate can be summed up by these truisms:

  1. The best SPA is better than the best MPA.
  2. The average SPA is worse than the average MPA.

I think that’s a fair take, but there are a couple other architectures still remaining that make things a little blurrier.

Islands Frameworks

Recently we’ve seen the emergence of a new category: server-side rendered multi-page frameworks that embrace islands of interactivity Islands Architecture The islands architecture encourages small, focused chunks of interactivity within server-rendered web pages www.patterns.dev/vanilla/islands-architecture for rich client-side behavior. While the idea itself isn’t new, the current crop of frameworks built around it are — Astro, Deno Fresh and Enhance, among others.

In case you’re unfamiliar: an island of interactivity is a region of an otherwise static HTML page that is controlled by JavaScript. It’s an acknowledgment that while richly interactive applications do exist, the richly interactive part is often surrounded by a more traditional website. The classic example is a carousel, but the pattern is broadly useful; the interactive demos on this very blog Web Components Will Outlive Your JavaScript Framework | jakelazaroff.com If we're building things that we want to work in five or ten or even 20 years, we need to avoid dependencies and use the web with no layers in between. jakelazaroff.com/words/web-components-will-outlive-your-javascript-framework/ are built as islands within static HTML.

What that means in practice is that these websites will fit mostly into the bottom left quadrant — except for the namesake islands of interactivity, which fit into the bottom right.

A graph with two axes that intersect in the exact center, labeled SSR/CSR horizontally and SPA/MPA vertically. A shaded region labeled “Islands Frameworks” covers the MPA half, covering both SSR and CSR.

Similar to JavaScript metaframeworks, islands frameworks also try to get the “best of both worlds” between client-side and server-side rendering — albeit as MPAs rather than SPAs. The bet is that reducing complexity around the static parts of a page is a better tradeoff than giving developers more control. As with traditional web frameworks, the gap between them should narrow as support for view transitions gets better.

Partial Swapping

This pattern is less all-encompassing than some of the others, but it’s worth mentioning because the past few years have seen it explode in popularity. By “partial swapping”, I mean making an HTTP request for the server to render an HTML fragment that gets inserted directly into the page.

To wit, websites using partial swapping generally fall on the server-side rendered side of the chart, spanning both the single-page and multi-page quadrants:

A graph with two axes that intersect in the exact center, labeled SSR/CSR horizontally and SPA/MPA vertically. A shaded region labeled “Partial Swapping” covers the SSR half, covering both SPA and MPA.

The most famous partial swapping tool is htmx, which people tend to use in conjunction with “traditional” server-side rendered frameworks. Other libraries like Unpoly and Turbo work similarly. Some frameworks in other categories, such as Rails (with Turbo) and Deno Fresh, have adopted partial swapping as well.

As I’ve written before, people act as though this pattern is saving the web from SPAs. Once we widen our view like this, though, we can see that’s a false dichotomy. In fact, by making it easier for developers to replace finer-grained regions of the page, partial swapping is actually a tool for creating SPAs3 — albeit server-side rendered ones.

It’s not all or nothing! The htmx documentation outlines how this pattern can work in conjunction with client-side scripting approaches such as islands </> htmx ~ Hypermedia-Friendly Scripting htmx.org/essays/hypermedia-friendly-scripting/ . I won’t make a chart with three of the four quadrants filled in, but you get the idea: these boundaries are fluid, and good tools don’t lock developers into a specific region.

Partial swapping can also be used as a polyfill for cross-document view transitions. Frameworks like Astro allow authors to load full pages asynchronously, progressively enhancing MPAs into server-side rendered SPAs.

Did We Learn Anything?

None of this is particularly groundbreaking. But I agree with Thomas that imprecise terminology doesn’t help whatever discourse plays out on the hot-take-fueled Internet argument fora. Hopefully, this can serve as a reference point when we talk about when and where these architectures are appropriate.

Footnotes

  1. Not to be confused with Meta Frameworks, which just means React.

  2. Dan Abramov gives an example of this in The Two Reacts The Two Reacts — overreacted UI = f(data)(state) overreacted.io/the-two-reacts/ . Imagine a blog with posts written in Markdown. An app fetching those posts from across the network and rendering them on the client would need to include a full Markdown parser in the JavaScript bundle. React Server Components allow the server to parse the Markdown, and send only the result to the client to be rendered.

  3. Of course, you don’t have to use partial swapping to create a full-on SPA. In Less htmx is More Less htmx is More How to build great websites with htmx by learning a couple browser features alongside it. unplannedobsolescence.com/blog/less-htmx-is-more/ , htmx maintainer Alexander Petros advocates using it judiciously and relying on regular links and form submissions that cause the browser to do a full-page navigation (in other words, progressive enhancement).