{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreicflmyayrwj6cb5drdehyfzhcpp4creuk34gxwwbewlxryojcg6du",
"uri": "at://did:plc:vrrdgcidwpvn4omvn7uuufoo/app.bsky.feed.post/3la7r36teb42t"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreiccivxclypue3qobugkd75ocmcpxaphczrm7hioaskuq4eer7a624"
},
"mimeType": "image/png",
"size": 34259
},
"description": "The web development community talks a lot about single-page apps, but are we all on a single page? Here's my ontology of web app architectures, organized by rendering and navigation.",
"path": "/words/whats-a-single-page-app/",
"publishedAt": "2024-11-05T00:00:00Z",
"site": "at://did:plc:vrrdgcidwpvn4omvn7uuufoo/site.standard.publication/3mmyfl3pxzi2a",
"tags": [
"html",
"javascript"
],
"textContent": "The web development community talks a lot about single-page apps, but are we all on a single page?\n\nHeydon Pickering tackled this question in his similarly-named article What Is A Single-Page Application?\nThe 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.\n\nThat's obviously tongue-in-cheek, but it's a reaction to the working definition that most people use.\nFor better or worse, \"single-page app\" is usually a euphemism for \"JavaScript framework app\".\n\nI recently wrote about building a single-page app with htmx using service workers to render everything client-side — no loading spinners in sight!\nIn response, Thomas Broyer objected to the premise that htmx and single-page apps were opposites.\nHe showed me an article that he wrote called Naming things is hard, SPA edition (which you should also go read!) that breaks down rendering into a spectrum.\n\nIn a bid to cement my burgeoning reputation as a Quadrant Chart Guy, I feel compelled to add even more nuance to the situation:\n\nI'm sorry.\nKinda.\n\nOkay, let's define the extrema of each axis:\n\nServer-side rendering (SSR) is when HTML is produced on a server and sent to the browser.\n\nClient-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.\n\nA 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.\n\nA 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.\n\nIf you just came here for an answer to the title, that's it; I guess you can go home now.\nBut I think it's interesting to look at the various tools people use and how they fit in.\n\nMost tools for building websites don't lock you into just one quadrant.\nAfter 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).\n\nSo: without casting any aspersions, here's my ontology of web app architectures organized by rendering and navigation.\n\nTraditional Web Frameworks and Static Site Generators\n\nThis is a pretty large tent, encompassing WordPress, Django, Rails (pre-Turbolinks) Jekyll, Hugo, Eleventy and myriad others.\nIt also includes hand-authored HTML, though I wouldn't describe that as a \"tool\" so much as a \"way of life\".\n\nTools in this category are on the bottom left of the chart: server-side rendered multi-page apps.\n\nThe tradeoffs of this quadrant are well known:\n\nThe browser takes care of a lot of important accesibility features, such as letting screen readers know when the user navigates to a new page.\n\nDelivering HTML first allows the content to be visible even if CSS or JavaScript fail to load.\n\nPages can load even faster if HTML is streamed in, rather than delivered all at once.\n\nThe full page must be downloaded and replaced on each navigation.\n\nIn fact, every interaction requires a network round trip.\n\nThis experience has remained mostly unchanged for 30 years.\nAnd it's great!\nWith only a little bit of HTML and CSS, you can make a pretty good website; the many Motherfucking Website variations show just how far a few tags and properties get you.\nThe low barrier to entry is one of the main reasons the web flourished.\n\nThree decades on, improvements in HTML and CSS are starting to mitigate some of the downsides.\nPreloading resources, for example, allows the browser to preemptively download associated files, which can make navigation almost instantaneous.\nAnd cross-document view transitions — not yet well supported, but hopefully soon! — promise to allow multi-page apps to navigate with fancy animations.\n\nThat said: requiring a network request and a whole new page for every interaction is a pretty strong constraint!\nAs developers' ambitions grew, they leaned more and more heavily on JavaScript, which led to…\n\nJavaScript Frameworks\n\nAlthough JavaScript was invented way back in 1995, I don't think a schism truly happened until 2010 or so.\nThat'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.\n\nNote that I'm not talking about Next.js or similar tools (I'll get to them in the next section).\nI'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.\n\nUsed thusly, JavaScript frameworks are the diametric opposite of traditional web frameworks: both navigation and rendering happens on the client.\nAs such, they fit neatly into the top right quadrant: client-side rendered single-page apps.\n\nWhat are the benefits of this quadrant?\n\nThe initial page load can be much faster once the JavaScript bundle is cached.\n\nPage navigations can be instantaneous, since all the relevant state is already on the client.\n\nElements can persist across navigations, enabling e.g. uninterrupted media playback and fancy transition animations.\n\nModifying the UI without first going through the network enables much richer client-side interactions.\n\nIn practice, I think many of the purported benefits of client-side rendered SPAs turned out to be wishful thinking:\n\nWhen 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.\n\nPage navigations tend to wait for API responses from a server and database in the same datacenter that would have served the HTML anyway.\n\nAspirations of being richly interactive are often fantasy; most websites are really just gussied up forms.\n\nThere are also more general drawbacks:\n\nThe client needs to download 100% of the UI code.\n\nThe initial page load (before the JavScript bundle is cached) will always be slower.\n\nPage navigations are not accessible by default.\n\nIt's way more difficult for apps here to be indexed by search engines.\n\nIf I sound critical of this category, it's only because the industry has largely recognized these drawbacks and moved on to other architectures.\nWhile 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.\n\nClient-side rendered SPAs still have their uses, though.\nWhen I made my local-first trip planning app, I built it as a client-side rendered SPA.\nThere 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!\nAs 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.\n\nJavaScript Metaframeworks\n\nJavaScript frameworks had about half a decade of client-side rendering glory before people realized that delivering entire applications that way was bad for performance.\nTo address that, developers starting building metaframeworks — Next.js, Remix, SvelteKit, Nuxt and Solid Start, among others — that rendered on the server as well.\n\nIn metaframeworks, rendering happens in two different ways:\n\nWhen 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.\n\nNext, 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.\n\nThese steps slot neatly into the top left and top right quadrants, respectively:\n\nJavaScript 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.\nIn particular, they fix the cold cache initial page load and SEO drawbacks of the latter.\nWith React Server Components, React-based metaframeworks can omit UI code from the JavaScript bundle as well.\n\nDepending 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).\nBut \"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.\n\nA couple years ago, Nolan Lawson attempted to bridge the two camps:\n\nAt the risk of grossly oversimplifying things, I propose that the core of the debate can be summed up by these truisms:\n\nThe best SPA is better than the best MPA.\n\nThe average SPA is worse than the average MPA.\n\nI think that's a fair take, but there are a couple other architectures still remaining that make things a little blurrier.\n\nIslands Frameworks\n\nRecently we've seen the emergence of a new category: server-side rendered multi-page frameworks that embrace islands of interactivity for rich client-side behavior.\nWhile the idea itself isn't new, the current crop of frameworks built around it are — Astro, Deno Fresh and Enhance, among others.\n\nIn case you're unfamiliar: an island of interactivity is a region of an otherwise static HTML page that is controlled by JavaScript.\nIt's an acknowledgment that while richly interactive applications do exist, the richly interactive part is often surrounded by a more traditional website.\nThe classic example is a carousel, but the pattern is broadly useful; the interactive demos on this very blog are built as islands within static HTML.\n\nWhat 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.\n\nSimilar 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.\nThe bet is that reducing complexity around the static parts of a page is a better tradeoff than giving developers more control.\nAs with traditional web frameworks, the gap between them should narrow as support for view transitions gets better.\n\nPartial Swapping\n\nThis 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.\nBy \"partial swapping\", I mean making an HTTP request for the server to render an HTML fragment that gets inserted directly into the page.\n\nTo 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:\n\nThe most famous partial swapping tool is htmx, which people tend to use in conjunction with \"traditional\" server-side rendered frameworks.\nOther libraries like Unpoly and Turbo work similarly.\nSome frameworks in other categories, such as Rails (with Turbo) and Deno Fresh, have adopted partial swapping as well.\n\nAs I've written before, people act as though this pattern is saving the web from SPAs.\nOnce we widen our view like this, though, we can see that's a false dichotomy.\nIn fact, by making it easier for developers to replace finer-grained regions of the page, partial swapping is actually a tool for creating SPAs — albeit server-side rendered ones.\n\nIt's not all or nothing!\nThe htmx documentation outlines how this pattern can work in conjunction with client-side scripting approaches such as islands.\nI 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.\n\nPartial swapping can also be used as a polyfill for cross-document view transitions.\nFrameworks like Astro allow authors to load full pages asynchronously, progressively enhancing MPAs into server-side rendered SPAs.\n\nDid We Learn Anything?\n\nNone of this is particularly groundbreaking.\nBut I agree with Thomas that imprecise terminology doesn't help whatever discourse plays out on the hot-take-fueled Internet argument fora.\nHopefully, this can serve as a reference point when we talk about when and where these architectures are appropriate.",
"title": "What's a Single-Page App?"
}