Originally published on my website
Single-page apps are all the rage nowadays. Many praise their vague technical benefits while ignoring tremendous development costs.
In this article, we'll discuss why a single-page app is almost always worse than a multi-page app and briefly cover alternatives that can yield similar benefits without the costs.
Every business has two sides: revenues and costs. Whether an SPA is a good investment compared to alternatives depends on how it affects the bottom line.
Revenue depends on value delivered to users which in turn depends primarily on the feature set. Architectural choices don't provide value to users per se. The promise of SPAs is better user experience which may translate into higher revenue. This increase must be compared with the corresponding increase in costs to assess whether the investment is worthwhile.
The article attempts to prove the costs of an SPA are tremendous compared to an MPA mainly because of greater incidental complexity. However, many companies blindly assume user experience is improved enough to justify the extra expense. Others base their choice on some vague sense of engineering purity without considering business factors.
There are two key takeaways from the article:
- Do not consider the SPA architecture unless there's evidence user experience is the number one problem of your app and even in that case consider alternatives. For example, if you need to make the app snappier then you may be able to reap the bulk of the benefit by tuning your database, caching, using a CDN, etc.
- An MPA is a competitive advantage.
Let's take a look at the cost side.
Architectural choices affect different aspects of development in different ways. That's why I compiled a list of areas negatively affected by the SPA architecture. You can use it to assess the impact an SPA has or would have on your project.
Let's emphasize a clear pattern: an SPA negatively affects most items on the list and requires extra work to regain capabilities present in MPAs by default.
Here's the list starting with the most expensive items:
- I think this is a very underappreciated aspect of SPAs. Stateful software is always more difficult to work with than stateless. The frontend state is added on top of the already-existing backed state. This requires more development time, increases the risk of bugs, and makes troubleshooting more difficult.
- The stateful nature of the frontend greatly increases the number of test cases we need to write and maintain. Additionally, the test setup is more complicated because we need to make the backend and frontend talk to each other.
- It's frequently claimed SPAs offer better performance but it's more complicated than commonly thought. An API-only backend renders and sends less data than an MPA but the network latency is still there and the app won't be faster than that. We could work around the issue by implementing optimistic updates but this greatly increases the number of failure modes and makes the app more complex.
- Slow First-Time Load
- This is a well-known problem that isn't fully understood. The usual claim is that after the browser caches the asset bundle everything will be snappy. The implicit assumption is we've stopped development and don't update the bundle. If we do then users may experience quite a lot of first-time loads in a single day.
- This is optional for an SPA but it seems JWTs are a frequent choice for authentication. The claimed benefit is statelessness. It's all true but has a serious downside: we can't invalidate sessions unless we identify them on the backend which makes the system stateful. I think we always should be able to invalidate sessions. Therefore, because we need server-side state, we can simply use bearer tokens. They're simpler to understand, implement and troubleshoot.
- Session Information
- Again, this is optional but SPAs often use local storage instead of cookies. Its tremendous downside is lack of a mechanism similar to HTTP-only cookies. Given web apps often include scripts from third-party domains and CDNs a successful attack against them can leak session IDs and other secrets.
- State Updates
- Let's illustrate this with an example: we're building an e-commerce site that has a list of categories. We need to update the list from time to time. In an MPA, the list is updated on every page load. That's not the case in an SPA though. We need to think about an algorithm and implement it. It's not rocket science but it's busywork that users don't care about.
- Error Handling
- An MPA renders a 500 page upon error and that's it. However, an SPA needs to detect errors in the client code and then update the user interface accordingly. Again, busywork required to regain what MPAs offer out of the box.
- Server-side Rendering
- We may need server-side rendering so that users are able to discover the app. This is yet another area where you need to do work to match the capabilities of an MPA.
- Protocols and Serialization
- In an MPA, we can simply pass models to views and render attributes we need. This isn't the case in an SPA. We need to define data formats and implement serialization. Naturally, there are tools that can help but it's extra work and dependencies whose only effect is regaining the convenience of an MPA.
- Our build system may become more complicated because of the additional tooling and dependencies required to build an SPA.
- Shared Metadata
- We may need to share data between the frontend and the backend. For example, if the SPA consumes a REST API then we would like routing information to be derived from the same source. Again, this is unnecessary in an MPA.
If you look at the SPA architecture from a business perspective then your costs will go up but you won't see more money coming in because users don't care about technical choices. The result is a negative return on investment.
My advice is simple: if it hurts then stop doing it. Even better: don't start doing it in the first place.
A multi-page app is much simpler to implement and has many advantages that can only be replicated in an SPA at a huge cost. Naturally, more complicated components are sometimes unavoidable but there are more sensible approaches.
First, start using Turbolinks. It'll make the app feel snappier without injecting a ton of incidental complexity. It's often associated with Ruby on Rails but can be easily used independently with other technologies.
Second, use Stimulus.js for simpler components. It's a relatively new development but I had a chance to implement a dozen Stimulus controllers and the experience was great.
Third, if you're implementing a very complicated component then you can use React just for that component. For instance, if you're building a chat box then there's really no need to implement your login page in React. The same applies to Vue.js and the rest of the pack.
Single-page apps are much more expensive to build than multi-page apps. In most cases, there is no business reason to choose this architecture. The problems it's trying to solve can be address in simpler ways without excessive costs and complexity. There are cases where an SPA makes sense but this is a topic for another article.