During the past few months, social networks have been shaken by a single-page vs multi-page applications (SPA vs MPA) battle, more specifically related to Next.js and React, following, among other things, a tweet by Guillermo Rauch and a GitHub comment by Dan Abramov.
I've read a few articles and been involved in a few discussions about those and it appeared that we apparently don't all have the same definitions, so I'll give mine here and hope people rally behind them.
SPA vs MPA: it's about navigation
It's not that hard: a single-page application means that you load a page (HTML) once, and then do everything in there by manipulating its DOM and browser history, fetching data as needed. This is the exact same thing as client-side navigation and requires some form of client-side routing to handle navigation (particularly from history, i.e. using the back and forward browser buttons).
Conversely, a multi-page application means that each navigation involves loading a new page.
This by itself is a controversial topic: despite SPAs having lots of problems (user experience –aborting navigation, focus management, timing of when to update the URL bar–, accessibility, performance even by not being able to leverage streaming) due to taking responsibility and having to reimplement many things from the browser (loading feedback, error handling, focus management, scrolling), some people strongly believe this is “one of the first interesting optimizations” and they “can’t really seriously consider websites that reload page on every click good UX” (I've only quoted Dan Abramov from the React team here, but I don't want to single him out: he's far from being alone with this view; others are in denial thinking that “this is the strategy used by most of the industry today”). Some of those issues are supposedly (and hopefully) fixed by the new navigation API that's currently only implemented in Chromium browsers. And despite their many advantages, MPAs aren't free from limitations too, otherwise we probably wouldn't have had SPAs to being with.
My opinion? There's no one-size-fits-all: most sites and apps could (and probably should) be MPAs, and an SPA is a good (and better) fit for others. It's also OK to use both MPA and SPA in a single application depending on the needs. Jason Miller published a rather good article 4 years ago (I don't agree with everything in there though). Nolan Lawson also has written a good and balanced series on MPAs vs SPAs.
And we haven't even talked about where the rendering is done yet!
Rendering: SSR, ESR, SWSR, and CSR
Before diving into where it's done, we first need to define what rendering is.
My definition of rendering is applying some form of templating to some data.
This means that getting some HTML fragment from the network and putting it into the page with some form of innerHTML
is not rendering.
Conversely, getting some virtual DOM as JSON for example and reconstructing the equivalent DOM from it would qualify as rendering.
Now that we've defined what rendering is, let's see where it can be done: basically at each and any stage of delivery: the origin server (SSR), edge (ESR), service-worker (SWSR), or client (CSR).
There's also a whole bunch of prerendering techniques: static site generation (SSG), on-demand generation, distributed persistent rendering (DPR), etc.
All these rendering stages, except client-side rendering (CSR), generate HTML to be delivered to the browser engine.
CSR will however directly manipulate the DOM most of the time, but sometimes will also generate HTML to be used with some form of innerHTML
; the details here don't really matter.
Rendering at the origin server or at the edge (Cloudflare Workers, Netlify Functions, etc.) can be encompassed under the name server-side rendering (SSR), but depending on the context SSR can refer to the origin server only. Similarly, rendering in a service worker could be included in client-side rendering (CSR), but most of the time CSR is only about rendering in a browsing context. I suppose we could use browser-side rendering (BSR) to encompass CSR and SWSR.
As noted by Jason Miller and Addy Osmani in their Rendering on the Web blog post, applications can leverage several stages of rendering (SSR used in the broader sense here), but like many they conflate SPA and CSR. Eleventy (and possibly others) also allows rendering a given page at different stages, with parts of the page prerendered at build-time or rendered on the origin server, while other parts will be rendered at the edge.
What does that imply?
My main point is that rendering is almost orthogonal to single-page vs multi-page: an SPA doesn't imply CSR.
- Most web sites are MPAs with SSR, sometimes ESR.
- Most React/Vue/Angular applications are SPAs with CSR: the HTML page is mostly empty, generally the same for every URL, and the page loads data on boot and renders it (at the time of writing, the Angular website is such an SPA+CSR).
- Next.js/Gatsy/Remix/Nuxt/Angular Universal/Svelte Kit/Solid Start/îles applications are SPAs with SSR and CSR: data is present as HTML in the page, but navigations then use CSR staying on the same page (and actually, despite the content being present in the HTML page, those frameworks will discard and re-render it client-side on boot).
- Qwik City/Astro/Deno Fresh/Enhance/Marko Run applications are MPAs with SSR (and CSR as needed through islands of interactivity); Qwik City provides an easy way to switch to an SPA with SSR and CSR (though contrary to the above-mentioned frameworks, Qwik City won't re-render on page load).
- Hotwire Turbo Drive (literally HTML over the wire; formerly Turbolinks) and htmx applications are SPAs with SSR.
- GitHub is known for its use of Turbolinks and is actually both MPA and SPA, depending on pages and sometimes navigation (going from a user profile to a repository loads a new page, but the reverse is a client-side navigation).
Some combinations aren't really useful: an MPA with CSR (and without SSR) would mean loading an almost empty HTML page at each navigation to then fetch data (or possibly getting it right from HTML page) and do the rendering. Imagine the Angular website (which already makes a dubious choice of not including the content in the HTML page, for a documentation site) but where all navigations would load a new (almost empty) page.
Similarly, if you're doing a SPA, there's no real point in doing rendering in a service worker as it could just as well be done in the browsing context; unless maybe you're doing SPA navigation only on some pages/situations (video playing?) and want to leverage SWSR for all pages including MPAs?
Other considerations
In an application architecture, navigation and rendering locality aren't the only considerations.
Inline updates
Not every interaction has to be a navigation: there are many cases where a form submission would return to the same page (reacting to an article on Dev.to, posting a comment, updating your shopping cart), in which case progressive enhancement could be used to do an inline update without a full page refresh.
Those are independent from SPAs: you can very well have an MPA and use such inline updates. Believe it or not, this is exactly what Dev.to does for their comment form (most other features like following the author, reacting to the post or a comment, or replying to a comment however won't work at all if JavaScript is somehow broken).
Concatenation and Includes
Long before we had capable enough JavaScript in the browser to build full-blown applications (in the old times of DHTML, before AJAX), there already were optimization techniques on the servers to help build an HTML page from different pieces, some of which could have been prerendered and/or cached. Those were server-side includes and edge-side includes.
While they are associated with specific syntaxes, the concepts can be used today in edge functions or even in service workers.
The different parts being concatenated/included this way can be themselves static or prerendered, or rendered on-demand. Actually the above-mentioned feature of Eleventy where parts of a page are server-rendered or prerendered and other parts are rendered at the edge is very similar to those includes as well.