TL;DR. The SPA path is dark and full of terrors. You can bravely fight those… or choose a different path that takes you to a similar destination: modern Rails.
I remember thinking that Rails was focusing on the wrong target when DHH announced Turbolinks in 2012. My conviction back then was that offering an instant response time to user interactions was key to excellent UX. Because of network latency, such interactivity is only possible if you minimize your dependency on it and, instead, manage a lot of state on the client.
I thought this was necessary for the kinds of apps I was working on. And with that in mind, I tried many approaches and frameworks for implementing the same pattern: Single-page applications (SPA). I believed that the SPA wagon was the future™. A few years of experience later, I am not sure what the future is, but I really want to have an alternative.
The SPA rabbit hole
These apps feel more like a native app than a traditional web page, where you depend on the server for responding to interactions. For example, if you have used Trello, you can see how fast it feels to create cards.
Of course, with great power comes great responsibility. In a traditional web app, your server application includes your domain model and rules, some data-access technology to interact with the database, and a controller layer that orchestrates how HTML responses are rendered in response to HTTP requests.
With SPA things are a bit more complex. You still need a server-side app including your domain model and rules, a web server, a database and some data-access technology… and a bunch of additional things on top of that:
For the server:
- An API satisfying the data needs of your new client
- A JSON serialization system for interchanging data with the client and some caching system that supports it
- A templating system for transforming data into HTML
- A representation of your domain model and rules
- A new persistence layer in the client for propagating changes to the server
- A system for updating views when data changes
- A system for associating URL routes to screens (unless you want to use the same URL for everything, which doesn’t feel much web-ish)
- A system for bootstrapping all the components that are needed for rendering screens, and for fetching the necessary data to do it
- A new pattern/architecture to organize all this code
- A system for dealing with errors, logging, exception tracking, etc.
- A system for generating JSON for your server requests
- An automated-testing framework supporting your SPA
- An additional suite of tests to write and maintain
- Additional toolchain for building, packaging and deploying the new app
In sum, with an SPA you have an additional app to maintain. And a whole new range of problems to deal with. And notice you are not replacing one app with other. You still need the server-side app (it will just render JSON instead of HTML now).
API and data interchanges
The data interchanges between your new app and your server is a complex problem to solve. There are two opposing forces:
- You will want to make as little requests to the server as possible for performance reasons
- Serializing a single kind of record as JSON is easy. But weaving big models of different records, to minimize the numbers of requests is not. You will need design carefully your serialization logic to optimize the database queries and keep things performant.
On top of that, you will need to think about what to fetch and when to do it on a per-screen basis. I mean, you need to balance load time, what is required right away, what can be fetched lazily, and come with an API design that satisfies that.
Some standards might help here. JSON api, to standardize the JSON format you use for your data; or GraphQL, for fetching precisely the data you want, as complex as you need, in a single request. But none of them will save you from:
- Designing every data interchange
- Implementing the queries satisfying each interchange efficiently on your server.
And both aspects represent a considerable amount of additional work.
Initial load time
People associate Single-Page apps to speed, but the truth is that making them load fast is challenging. The reasons are multiple:
- On top of the initial HTTP request for loading the app, they typically require one request or more for fetching the JSON data you need for rendering the screen.
- The client needs to convert the JSON into HTML before rendering anything. Depending on the device and amount of JSON to transform, this can introduce a noticeable delay.
This doesn’t mean it is not possible to make SPAs load fast. I just say it is difficult and something that you need to plan for, since doesn’t come up naturally with the pattern.
For example, Discourse, an Ember-based SPA, has a fantastic startup time but, among other things, they preload a bunch of JSON data as part of the initial HTML that is served, to prevent further requests. And notice the discourse team is proudly obsessed with speed and their skills are way above average. Keep that in mind before assuming you can easily replicate such performance in your SPA.
This approach requires running a JS runtime on the server and is not free of technical challenges. For example, developers have to plan for the load-time events used in your SPA, since the load process will change now.
I like the sharing-code possibilities of this idea, but I haven’t seen an implementation that didn’t make me run in the opposite direction. I also find this page-rendering process kind of funny:
- Server: Query your server-side API
- Server: Query your database
- Server: Generate JSON
- Server: Convert JSON into HTML
- Client: Render the initial HTML
- Client: Load the SPA
- Client: Parse the initial HTML and hydrate the DOM
Couldn’t you just query the database, generate HTML and go?
This is not entirely fair, because you wouldn’t go SPA and because most of this sorcery is hidden by the framework, but it still feels wrong to me.
Coding apps with rich graphical user interfaces is hard. It is with good reason that they were one of the problems that inspired object-orientation and many design patterns.
Managing a lot of state in the client is difficult. Traditional websites typically focus on purpose-specific screens that get discarded to start with a fresh state when a new one is loaded. An SPA, on the other hand, is responsible for managing all the state and screen updates during the time is used, and it has to make sure everything moves consistently and smoothly.
There are as many different architectures as SPA frameworks out there:
- Most frameworks diverge from the traditional MVC pattern. Ember was initially heavily inspired by Cocoa MVC but changed its programming model quite a bit in recent versions.
- There is a tendency to favor components over the traditional controller/view split (some of them, like Ember and Angular, did this in later major revisions).
- They all support some kind of one-way data-binding. 2-way data binding is discouraged because of the side-effects it introduces.
- Most frameworks include some routing system, which enables mapping URLs to screens, and determines how to instantiate components for rendering them. This is pretty unique to the web and something that didn’t exist in traditional desktop GUIs.
- Facebook’s flux architecture has had quite an impact in the industry, and containers like Redux, vuex and many others are heavily inspired by it.
Of all the frameworks I have seen, Ember is my favorite. I love its cohesiveness and that it’s opinionated. I also like how its programming model has evolved in recent versions, mixing traditional MVC, components, and routing.
On the other hand, I strongly dislike the Flux/Redux camp. I have seen so many smart people adopting it that I have made an effort to study and understand it, multiple times. I can’t avoid ending up shaking my head in disbelief when I see the code. I don’t see myself writing code following such pattern and being moderately happy at the same time.
Personal preferences apart, the bottom line is that if you pick the SPA route, you have a very complex problem to solve: architect your new app properly. And the industry is far from having an agreement on how to do that. Every year, new frameworks, patterns, and framework revisions appear that change programming models quite a bit. You will have to write and maintain a ton of code based on your architectural choices, so make sure you think about this thoroughly.
When working with SPAs, code duplication is likely to be a thing.
For your SPA logic, you will want a rich model of objects that represent your domain and its rules. And you still need the same for your server logic. And this recipe is just a duplication waiting to happen.
Invoice class that exposes a method
total that sums all the items so that you can render that amount. In the server, you also need an
Invoice class with a
total method, for calculating that amount when sending invoices by email. See? Client and server
Invoice classes implementing the same logic. Duplicated code.
And the same will happen if you want to share rendering templates between the server and the client. For example, what about if, for SEO purposes, you want to send a Server-side-generated version of your site when you detect a web crawler? You will have to write your templates again on the server, and make sure they stay synced from that moment on. Duplicated code again.
Having to replicate logic or templates in both the client and the server is a source of increasing programming unhappiness, in my experience. The first time you do it is ok. The 20th you do it, you will shake your head. The 50th you do it, you will wonder if all this SPA thing was necessary in the first place.
In my experience, building robust SPAs is far more challenging than writing robust server-side generated web apps.
First, no matter how careful you are, no matter how many tests you write. The more code you write, the more bugs you will have. And an SPA represents, sorry if I insist too much, a huge additional pile of code to write and maintain.
Second, as mentioned before, building rich GUIs is hard and results in complex systems composed of many elements interacting with each other. The more complex the system you write, the more bugs you will have. And compared to a traditional web app using a Model-2 variant of MVC, the complexity of an SPA is just crazy.
For example, for keeping data consistency in the server, you can leverage on database constraints, model validations and transactions. If something goes wrong, you respond with an error message. In the client, things are a bit more complicated. A lot can go wrong, just because a lot is going on. Maybe some record saves successfully, and some other record fails. Perhaps you go offline in the middle of some operation. You need to make sure the UI stays consistent at all moment, and that the app recovers gracefully when errors happen. All this is doable, of course, it’s just much harder.
This sounds silly but, for building SPAs, you need developers that know about how to do it. In the same way you should not underestimate the complexity of an SPA, you should not assume that any experienced web developer, with the right motivation and common sense, can write great SPAs from scratch. You need the right skills and experience or assume that important errors are going to be made. I know this because that was exactly my case.
This might represent a challenge for your company more significant than you think. The SPA approach encourages teams of specialists instead of generalists:
- SPA frameworks are intricate pieces of software that take countless flight hours to be productive with. Only the people from your company spending those hours will be able to maintain those apps.
- SPA frameworks require thorough and performant APIs. Building those demands entirely different set of skills than those needed for SPA frameworks.
Chances are that you end up with people who can’t work on the SPA and with people who can’t work on the server side, just because they don’t know how to.
This specialization might be a perfect fit for Facebook or Google and their teams composed of multiple layers of engineering troops. But would it be for your 6-people dev team?
There are three pieces included in modern Rails that might rewire your mind when it comes to designing modern web applications:
- One is Turbolinks, and I think it’s mind-blowing.
- The other is an old friend that is often overlooked these days: SJR responses and plain AJAX-rendering calls.
- And the last one is a recent addition: Stimulus
It is difficult to have an idea of how some approach feels without playing with it. Because of that, I will make a few references to Basecamp in the following sections. I have nothing to do with Basecamp, other than being a happy user. Regarding this article, it is just a good live example of modern Rails you can try for free.
The idea behind Turbolinks is simple: speed up your application by replacing full page loads with ajax requests that perform a
<body> replacement. The internal sorcery to make this work is hidden. As a developer, you can focus on a traditional server-side flow.
I used to have concerns about its performance. I was wrong. The speed gain is huge. What convinced me was using it in a project, but you can just start a trial in Basecamp and play around. Try to create a project with some elements and then navigate around by clicking the different sections. This will give you a good idea of how Turbolinks feels.
I don’t think Turbolinks is mind-blowing for its novelty (pjax is 8 years old). Or for its technical sophistication. What amazes me is how such as simple idea can improve your productivity by orders of magnitude, compared with the SPA alternative.
Let me highlight some of the problems it eliminates:
- Data interchanges. You don’t have them. No need to serialize JSON, design APIs or think about data queries that satisfy client needs in a performant manner.
- Initial load. Unlike SPA, it encourages fast load time by design. For rendering a screen, you can fetch the data you need directly from the database. And querying relational databases efficiently or caching HTML are well-solved problems.
MVC on the server, in the variant used by Rails and many other frameworks, is much simpler than any of the patterns used for architecting rich GUIs: receive a request, deal with the database to satisfy it and render a page of HTML as a response.
Finally, the constraint of always replacing the
<body> has a wonderful effect: you can focus on the initial rendering of pages, instead of updating specific sections (or updating some state, in the SPA world). In the general case, it just renders everything again.
- Code duplication. There is only one representation of your app that lives in the server. Your domain model, its rules, your app screens, etc. There is no need to duplicate concepts in the client.
Notice I am not talking about addressing problems, but about eliminating them. For example, GraphQL or SPA Rehydration are super-smart solutions to very complex problems. But what about if, instead of trying to find a solution, you put yourself in a situation where those problems don’t exist? That’s problem restatement at work. And it took me years to fully appreciate the power of this problem-solution approach.
- Turbolinks comes with its own custom “page load” event, and existing plugins relying on regular page loads won’t work. Today there are better ways to attach behavior to the DOM, but legacy widgets won’t work unless adapted.
- The speed is excellent, but it is not exactly like an SPA that can handle some interaction without fetching the server. I will talk more about tradeoffs later.
AJAX rendering and SJR responses
Remember when rendering HTML via Ajax was hot 15 years ago? Guess what? It still a wonderful resource to have in your toolbox:
- Fetching some HTML fragment from the server and adding it to the dom feels super-fast (like 100ms fast).
- You can render the HTML server-side, enabling you to reuse your views and fetch the required data directly from the database.
You can see how this approach feels in basecamp by opening your profile menu by clicking on the top-right button:
It feels instant. From the development side, you don’t have to care about JSON serialization and client-side rendering of stuff. You can just render that fragment on the server using all the Rails goodies.
I think many developers today look at Ajax rendering and SJR responses with disdain. I remember doing that too. They are a tool, and as such, can be abused and misused. But when used right, they are a terrific solution. The let you offer great UX and interactivity at a very low cost. Sadly, as Turbolinks, it is difficult to appreciate them unless you have fought a few SPA battles first.
- It leverages on
MutationObserverfor attaching behavior to the DOM, meaning it doesn't care about how the HTML appears in the page. Of course, this plays perfectly with Turbolinks.
- It saves you a bunch of boilerplate code for attaching behavior to the DOM, for attaching handlers to events and for locating elements within a given container.
- It encourages keeping the state in the DOM. Again, this means it does not care about how the HTML is generated, which is suitable for many scenarios, including Turbolinks.
I have used Stimulus in a few projects, and I like it a lot. It removes a bunch of boilerplate code, it is built on latest web standards and reads very nicely. And something I love especially: it is now the standard way of doing something that, until now, what up to each app to decide how to do.
A game of tradeoffs
Turbolinks is usually marketed as “Get all the benefits of SPA without any of its inconveniences”. I don’t think this is entirely true:
- Apps built with modern Rails feel fast, but an SPA will still feel faster for interactions that do not rely on the server.
- There are scenarios where SPA makes more sense. If you need to offer a high level of interactivity, have to manage a lot of state, execute complex client-side logic, etc. an SPA framework will make your life easier.
Now, development is a game of tradeoffs. And in this game:
- Modern Rails lets you build apps that are fast enough and feel great.
- For a vast variety of apps, Rails enables you to implement the same functionality with a fraction of the code and complexity.
I believe that with Rails you can get 90% of what an SPA offers with a 10% of the effort. Regarding productivity, Rails kills SPA. In terms of UX, I think many developers make the same mistake I made of assuming that SPA UX is unbeatable. It is not. In fact, as discussed above, you better know what you are doing when building your SPA, or the UX will actually be worse.
I observe companies adopting SPA frameworks in mass, and countless articles about doing fancy things the SPA way. I think there is a lot of “not using the right tool for the job” going on, as I firmly believe that the types of apps that justify SPA are limited.
And I say justify because SPAs are hard. If anything, I hope I have convinced you of that in this article. I am not saying that it is impossible to create great SPAs, or that modern Rails apps are great by definition, only that one path is super-hard, and the other is much easier.
While researching for this article, I stumbled into this tweet:
It made me laugh because I would go with the first options unless the alternative was justified. It is also representative of a kind of developer-mindset that adores complexity and thrives in it, to the point of considering insane other people with different criteria.
Over the years I have learned the hard way that complexity is often a choice. But in the programming world, it is surprisingly difficult to choose simple. We venerate complexity so much that accepting simplicity often implies thinking different which, by definition, is hard.
Remember that you can choose to put yourself out of trouble. If you choose the SPA path, make sure it is justified and that you understand the challenges. If you are not sure, experiment with different approaches and see for yourself. Maybe Facebook or Google, at their scale, don’t have the luxury of making such decisions, but you probably do.
And if you are a Rails developer that abandoned the Rails way many years ago, I encourage you to revisit it. I think you will be delighted.