Why You (Probably) Shouldn't Start With an SPA

1st Feb '2411 of your Earth minutes

I came across this interesting article by @gregnavis the other day. I guess it's from a few years ago now, judging by some of the other posts on his blog. But it still holds up. It's maybe even more relevant now.

The article is entitled The Architecture No One Needs and it makes a simple and clear case, arguing that SPA's are more expensive than a standard multi-page app (which may or may not be a monolith).

I'm going to use SPA throughout this post to mean the whole umbrella of ways you might be building a front-end application that is not server-side rendered.

I think this is in line with Greg's intent too.

I don't want to split hairs over whether a particular framework can be used to build front-end apps that aren't strictly SPAs.

Since I read it, I haven't stopped thinking about this article.

I found myself agreeing with all of Greg's stated points and it made me realise I actually have a strong opinion about this topic.

I believe Greg is right, and as time rolls on I think I'm becoming more bullish in my stance on SPAs too.

Some history, some context

My foray into the dirty, hubbub streets of front-end frameworks came about because of Laravel Nova and Statamic. They both use Vue, so I learned Vue.

Of course, I looked around. But React made me retch. Angular almost made me want to buy a katana just to perform seppuku. (Of course I'm being hyperbolic.)

I hear things about all of these and more thanks to Twitter. I can say some of it is good, but most of it continues to push me away.

I stuck with Vue. I actually like Vue a lot, even if v3 has caused a bit of headache—it's actually better for it in my opinion and yes I do like the composition API.

Overall though, I'm definitely heading more towards preferring not to have to build my front-end using node/deno/bun or whatever tool becomes popular today.

That said, you just try and pry Tailwind from my frigid, rigid fingers!

I'm quite firmly in the Livewire camp now. In another life, I may even like HTMX?

I've built a few SPAs, but I can probably count them on one hand. But not only will I probably not build another one, I think you shouldn't either. I'm strongly encouraging my clients not to.

I do think SPAs have a place, but it is almost certainly not in your project.

Yes, I know this argument has been made before and probably far more eloquently than I'm going to, but you know I just felt like I needed to get this out of my head in my own way.

When does an SPA make sense?

Let's get this out of the way first before I dunk on SPAs some more.

They do have a place:

I believe that the main advantage of an SPA is/was/has always been the decoupling of the front-end from the back-end.

As time goes on and engineers specialise in areas that interest them the most, roles become more well-defined. This is why we have 'front-end' and 'back-end' and 'full stack' engineers.

Although there is principally a lot of overlap (this is all programming at the end of the day and many concepts are similar), the domain—the environment that the engineer is most familiar with—is what determines their preference.

Some engineers will prefer back-end because they don't want to think about or deal with a certain class of bugs or issues that arise from the rapidly-evolving world of front-end development.

They may be uncomfortable using 3 or 4 different languages at the same time to get their work done, or they groan as soon as there's another major version of some framework which is going to require a load of refactoring that doesn't add immediate value to your product.

Consider: every user of your app could be using a different version of a particular browser, which uses one kind of rendering engine, and a specific level of conformity to various web standards e.g. ECMAScript (the official standard that underpins what most of us think of as JavaScript) or CSS. Keeping on top of these variations and differences across desktop and mobile is enough to make anyone's head spin.

And other engineers will prefer the front-end for its stateful nature, or because they're more comfortable with JavaScript/TypeScript, they grok CSS, and they love the intersection of code and design.

Or maybe they just dislike dealing with databases, concurrency, queues, messaging and APIs a lot more than they dislike wild browser evolution.

In any case, whatever size your team is, there will be these preferences. For example, I consider myself a full-stack developer, but honestly I like to avoid front-end work as much as possible, so will generally take the easy route there.

Developing an SPA could allow you/your business to split the work of front-end and back-end into separate teams, which may help each team focus so they can do what they do best and be the happiest they can be.

Which is the most important metric that will make an appreciable difference to your bottom-line long term.

Being intentional about this will see you hit Conway's Law head-on and potentially tackle that beast in the best way, as long as you build up the necessary lines of communication.

It also allows you to scale the two parts of the system separately—both in terms of team scaling and resource scaling—which, if you ever need that flexibility, could end up saving you from a certain group of headaches.

And I'm gonna be honest, building distributed systems like this is a cool problem to solve and will be a point of growth for many of your engineers.

So, why are SPAs bad again?

Yeh, so far this all sounds great, right? Well, just letting ol' Conway right into your living room isn't exactly the best idea.

Aside of all the points that Greg makes in his article (if you haven't read it yet, go read it), I want to present three extra reasons why I think an SPA is a bad idea.

Your front-end and back-end get decoupled!

Your back-end and front-end are always coupled. So trying to split them in anything but the most extreme circumstances is an exercise in futility.

I think this is probably the worst part of this whole story.

If your back-end team want to move in one direction, they've got to align with the front-end team. If timings and priorities don't work out, it's going to force someone to either put a hold on some work that really needs sorting out or do some grunt work just to patch over a hole that's about to appear.

This is communication overhead. It adds risk. It adds complexity. It adds meetings into engineers' calendars. It adds friction, and stress, and distraction.

It flies in the face of that number one principle: let your teams focus and be happy.

This literally costs you money one way or another, cost that you could avoid.

Deployments get unavoidably riskier in ways that are super difficult to test because testing distributed systems is really hard.

Again, this might all be fine, in the most extreme cases, where you need the decoupling. Then this extra expense, and complexity, and churn-causing evil, is just a necessary evil that you have to learn to swallow and live with.

But I've got x engineers, y1 are front-end, y2 are back-end. What do I do?

I would strongly recommend that one of your engineering groups need to roll up their sleeves and get on learning the other group's code, tooling and responsibilities.

This will have multiple benefits: career progression and learning opportunities, increased bus factor, fewer meetings and more collaboration.

Sure there will be challenges too, but they won't be as big or as painful as the other challenges you'd face with an SPA.

It may hurt customisability

As I mentioned earlier, the reason I got into Vue at all was because other tech I was using required me to. In both Statamic and Laravel Nova, the choice of Vue—well, not specifically Vue, but rather a reactive front-end framework—made sense because at the time it really was the best way to build flexible, reactive front-ends.

And both of those tools needed that power and have become fantastic tools because of it.

But there is one pain point that it's created that's quite hard to escape: the customisation story for each of these is harder because of it.

How so?

Basically, because each tool needs to build the assets to ship their product. And once they're built it's hard for third-parties to build on what's already there.

How is it harder? Let's say I'm creating a Statamic add-on that allows CMS owners to post to social media from their control panel. As Statamic uses Vue and already has a bunch of components I can leverage, I am going to use some of them.

But I'm also going to add some of my own functionality that doesn't already exist within Statamic. Now what happens? Well I build the Javascript... but wait. I can't change the bundled JS that's part of Statamic core.

I have to build my own JS and load it at the right time, something I'm not in control of. Thankfully the Statamic team (building on tooling from the Vue & JS community) have worked hard to make this relatively easy, but my tool choice is now limited by what they support - if they're using Vue 2, I have to use Vue 2; if I don't like Vite or Webpack, tough luck.

And on top of that, the builds have to happen at the client's end, which means we're now offloading some of the responsibility of making this whole thing work to people who don't need or want to know anything about this stuff. They just want to install your thing and get on with their jobs and lives.

Why is this such a pain though? It used to be (in other platforms) that I could just load some extra JS file into the admin interface and do what I want.

Honestly, we probably should never have been doing that either. Hands up, how many of you have seen a WordPress installation that tries to load 2 or more different versions of jQuery? 🙋🏼‍♂️

So these build tools go some way to alleviating some problems, but in the process have introduced so many layers of protection and abstraction that it presents a brain-melting, Japanese puzzle box to unlock.

And all this JavaScript flying around is really unsafe, because JavaScript is completely malleable on the client side.

That's meant library creators have had to go to some unusual lengths to protect the state of the application and encapsulate the code in attempts to make it safer and more portable.

I won't pretend to understand all of the requirements, pre-requisites and implications for why built JavaScript assets are packaged up in a complex soup of function calls and obfuscated code, but suffice to say this makes building on top of pre-existing code that much harder.

The web standards track is working to make this easier: we have Web Components and module starting to come through which should alleviate some of this.

But if you're not building with those standard in play—I understand, it might not be possible because of browser support etc—and you want to expose your user's to third-party plugins/add-ons, then you've got to figure out how to make it easy for other developers.

Some of that's going to require the specific implementation, the other part is going to be documentation. No matter how you cut it, it's going to be harder to get right than if you had server-side rendered views that you allow your third-party developers to load at runtime.

Performance will suffer

This isn't really an extra reason as Greg did touch on this a little already, but I wanted to go harder on performance.

You should never choose to build an SPA because of some supposed performance benefit.

That is the wrong hill to try and defend for many reasons, but primarily because you've got the whole of the web stack—on horses, with bazookas—nipping at your heels.

Sure, it may take a little while for web standards to get ratified and then rolled out, but the reality is that it's only a matter of time.

We now have wide browser support for QUIC / HTTP/3 (which brings faster downloads and reduced server load) and things like 103 Early Hints response headers (which let us tell browsers what they should prioritise pre-loading), making the standard, non-SPA web even more performant. (And yes, some of this benefits SPAs too!)

Sure, you can argue some of this advancement may have been driven by SPAs and their apparent benefits. But there's some inevitability to all of this (both the appearance of SPAs and the advancement of HTTP) which makes the whole argument moot in my opinion.

As adoption and overall performance of the web platform increases, SPAs will even start to feel slow in comparison. Some feel slow already!

That's because so much of the heavy lifting is left to the userland threads of the in-browser JavaScript engine instead of the lower-level compiled languages, the ones used to build the core browser engine itself (C++, Rust, Swift etc).

That translates to a poorer experience in your app, and a penalty for your users.

While it's not impossible for JavaScript to be as fast or faster than the actual browser it's running in, it's just such a long way for it to get there it's a no-brainer at this stage to let the browser do what the browser does best instead of trying to replicate all of that in JS.

So don't! Use JavaScript the way it was intended: as a sugar-coating to enhance the pages, not to build entire pages.

I mean, you wouldn't eat a donut made entirely out of sugar, would you?

Again, in the extreme cases, maybe you would. Maybe for this donut-sized/-shaped sugar torus, you have an appropriately-sized dough-only counterpart, both of which you consume in close proximity...

I donut-know where this analogy is going.

So what's the alternative?

You, dear reader, are not in the most extreme case. Probably not even close! And you may never be.

So, if you haven't started building an SPA, don't!

Keep your code together in a single application where the front-end is rendered by the back-end, and then you can test and deploy it as a single unit. And yes, you can even do that without Docker 😱

Create it as a fully server-rendered, multi-page application with page reloads and everything. Go on! I double dare you.

If you really want/need the reactivity, try something like Livewire, Hotwire or HTMX.

You can do this all the way up to many millions of users per day and it will be fine, which you are a long way from.

Trust me, your front-end will never meaningfully need to move faster than your back-end, and vice-versa.

If you're already running an SPA and are contemplating bringing the two parts of the donut back together, do it! Bite your lip, close your eyes, and just do it.

Relevant

#notadesigner • #sometimesitworks

All content licensed CC BY-SA 4.0  •  Code highlighting by Torchlight