Beyond pushState — building single page applications


Single page applications are on the rise, and in many cases they make the web faster, richer, and more interactive. The HTML5 history APIs have allowed us take advantage of the benefits of SPAs while still allowing pages to be deep linkable, server renderable, and easily shareable. But updating the page location is only the first part of the equation. The broader challenges are making the interaction feel right and supporting the inevitable complexities that will arise as you develop features.

If you fail to consider all the edge cases and complexities you will inevitably end off with an app that at best feels weird to users, and at worst feels buggy.

Here’s a look at some of the things you’ll need to keep in mind:

Fast back

Back should be quick; users don’t expect data to have changed much.

When a user presses the browser’s back button they expect the change to happen quickly and for the page to be in a similar state to how it was last time they saw it.

In the traditional web model the browser will typically be able use a cached version of the page and linked resources.

In a naive implementation of a SPA hitting back will do the same thing as clicking a link, resulting in a server request, additional latency, and possibly visual data changes.

In your SPA you need to think about these things too. Do you really need to go back to the server to re-render the page? What’s the cache policy for screens that might be used for fast-back? Is there some data that should be changed, for example after deleting an item?

Scrolling on history

History navigations should remember the scroll position.

Lots of sites get this wrong and it’s really annoying. When the user navigates using the browser’s forward or back button the scroll position should be the same as it was last time they were on the page. This sometimes works correctly on Facebook but sometimes doesn’t. Google+ always seems to lose your scroll position.

If the document body is the main scroll element then you will need to update the scroll position at the moment you are flipping screens. Get this wrong and you see a visual jump on the old screen.

If you move the scrollable region to an element associated with each screen, this makes some things easier and allow for Google+ style fade transitions without breaking scrolling.

Pending navigations

Block UI rendering until data is loaded.

For smooth transitions you should wait to render the next screen until its data has loaded. This can help avoid visual glitches and lead to better perceived performance.

A side effect of this, though, is that you need to have the notion of a “pending navigation”. You should be able to cancel pending navigations if they take too long, the request fails, or the user clicks another link.

beforeNavigateAway()

Avoid data loss by allowing navigations to be prevented.

In the traditional web, you can catch beforeunload to avoid user data being lost when the page changes. You’ll need a similar notion for your SPA. If the blocked navigation was a result of a history change event, you’ll need to make sure to fix the location.

Cacheable screens

Improve performance by keeping rendered pages around.

If there’s a page that is commonly visited, or whose data changes infrequently, it may make sense to cache the DOM permanently so that a navigation simply flips to the cached screen, if necessary you can update data in the background.

An additional thing to think about with cached screens is data consistency, and making sure changes that occurred elsewhere in the app are correctly reflected everywhere, cf. databinding.

Deferred code loading

Load code as late as possible.

To improve initial start up times you’ll want to minimize the amount of JS in the main bundle. This inevitably leads to cases when you want to navigate to a screen whose code has not loaded yet. One solution for this is to have a factory tied into the navigation pipeline, where the factory can asynchronously create the object responsible for managing the new screen.


For Medium we end off with something similar to the diagram below.


That’s about it for now. Let me know @dpup if you can think of other things people should be aware of.