State is hard: why SPAs will persist

When I write about web development, sometimes it feels like the parable of the blind men and the elephant. I’m out here eagerly describing the trunk, someone else protests that no, it’s a tail, and meanwhile the person riding on its back is wondering what all the commotion is down there.

We’re all building so many different types of products using web technology – e-commerce sites, productivity apps, blogs, streaming sites, video games, hybrid mobile apps, dashboards on actual spaceships – that it gets difficult to even have a shared vocabulary to describe what we’re doing. And each sub-discipline of web development is so deep that it’s easy to get tunnel-visioned and forget that other people are working with different tools and constraints.

This is what I like about blogging, though: it can help solve the problem of “feeling out the elephant.” I can offer my own perspective, even if flawed, and summon the human hive-mind to help describe the rest of the beast.

My last two posts have been a somewhat clumsy fumbling toward a new definition of SPAs (Single-Page Apps) and MPAs (Multi-Page Apps), and why you’d choose one versus the other when building a website. As it turns out, there is probably enough here to fill a book, but my goal is just to bring my own point of view (and bias) to the table and let others fill in the gaps with their comments and feedback.

I have a few main biases on this topic:

  1. I usually prize performance over ergonomics. I’ll go for the more performant solution, even if it’s awkward or unintuitive.
  2. I like understanding how browsers work, and relying on the “browser-y” way of doing things rather than inventing my own prosthetic solution.
  3. I don’t pay nearly enough attention to what’s happening in “user land” – I like to stay “close to the metal” and see the world from the browser’s perspective. Show me your compiled code, not your source code!

In thinking about this topic and reading what others have written on it, one thing that struck me is that a big attraction for SPAs is the same thing that can cause so many problems: state. People who like SPAs often celebrate the fact than an SPA maintains state between navigations. For instance:

  1. You have a search input. You type into it, click somewhere else to navigate, and the next page still has the text in the input.
  2. You have a scrollable sidebar. You scroll halfway down, click on something, and the next page still has the sidebar at the last scroll position.
  3. You have a list of expandable cards. You expand one of them, click somewhere else, and the next page still has the one card expanded.

Note that these kinds of examples are particularly important for so-called “nested routes”, especially in complex desktop UIs. Think of sidebars, headers, and footers that maintain their state while the rest of the UI changes. I find it interesting that this is much less of an issue in mobile UIs, where it’s more common to change (nearly) the whole viewport on navigation.

Managing state is one of the hardest things about writing software. And in many ways, this aspect of state management is a great boon to SPAs. In particular, you don’t have to think about persisting state between navigations; it just happens automatically. In an MPA, you would have to serialize this state into some persistent format (LocalStorage, IndexedDB, etc.) when the page unloads, and then rehydrate on page load.

On the other hand, the fact that the state never gets blown away is exactly what leads to memory leaks – a problem endemic to SPAs that I’ve already documented ad nauseam. Plus, the further that the state can veer from a known good initial value, the more likely you are to run into bugs, which is why a misbehaving SPA often just needs a good refresh.

Interestingly, though, it’s not always the case that an MPA navigation lands on a fresh state. As mentioned in a previous post, the back-forward cache (now implemented in all browsers) makes this discussion more nuanced.

Cache contents

A quick refresher: in modern browsers, the back-forward cache (or BF cache for short) keeps a cache of the previous and next page when navigating between pages on the same origin. This vastly reduces load times when navigating back and forth through standard MPA pages.

But how exactly does this cache work? Even an MPA page can be very dynamic. What if the page has been dynamically modified, or the DOM state has changed, or the JavaScript state has changed? What does the browser actually cache?

To test this out, I wrote a simple test page. On this page, you can set state in a variety of ways: DOM state, JavaScript heap state, scroll state. Then you can click a link to another page, press the back button, and see what the browser remembers.

As it turns out, the browser remembers a lot. I tested this in various browsers (Chrome/Firefox/Safari on desktop, Chrome/Firefox on Android, Safari on iOS), and saw the same result in all of them: the full page state is maintained after pressing the back button. Here is a video demonstration:

Note that the scroll positions on both the main document and the subscroller are preserved. More impressively, JavaScript state that isn’t even represented in the DOM (here, the number of times a button was clicked) is also preserved.

Now, to be clear: this doesn’t solve the problem of maintaining state in normal forward navigations. Everything I said above about MPAs needing to serialize their state would apply to any navigation that isn’t cached. Also, this behavior may vary subtly between browsers, and their heuristics might not work for your website. But it is impressive that the browser gives you so much out-of-the-box.

Conclusion

There are dozens of reasons to reach for an SPA technology, MPA technology, or some blend of the two. Everything depends on the needs and constraints of what you’re trying to build.

In these past few posts, I’ve tried to shed light on some interesting changes to MPAs that have happened under our very feet, while we might not have noticed. These changes are important, and may shift the calculus when trying to decide between an SPA or MPA architecture. To be fair, though, SPAs haven’t stopped moving either: experimental browser APIs like the Navigation API are even trying to solve longstanding problems of focus and scroll management. And of course, frameworks are still innovating on both SPAs and MPAs.

The fact that SPAs neatly simplify so many aspects of application development – keeping state in one place, on the main thread, persistent across navigations – is one of their greatest strengths as well as a predictable wellspring of problems. Performance and accessibility wonks can continue harping on the problems of SPAs, but at the end of the day, if developers find it easier to code an SPA than the equivalent MPA, then SPAs will continue to be built. Making MPAs more capable is only one way of solving the problem: approaching things from the other end – such as improved tooling, guidance, and education for SPA developers – can also work toward the same end goal.

As tempting as it may be to pronounce one set of tools as dead and another as ascendant, it’s important to remain humble and remember that everyone is working under a different set of constraints, and we all have a different take on web development. For that reason, I’ve come around to the conclusion that SPAs are not going anywhere anytime soon, and will probably remain a compelling development paradigm for as long as the web is around. Some developers will choose one perspective, some will choose another, and the big, beautiful elephant will continue lumbering forward.

Next in this series: SPAs: theory versus practice

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.