Back Original

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:

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:

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?

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

There are also more general drawbacks:

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.