Using Local Storage to Improve Page Load

Many devices these days support local storage, meaning that we can store assets in local storage on the first visit by the user to a given URL, and then on subsequent visits choose to retrieve assets from local storage rather than downloading them again.

Steve Souders has done a quick survey on the use of local storage and, combined with a cookie, this technique of retrieving assets from local storage can be useful in reducing the size and number of assets that are retrieved remotely. Currently, it's being used by Google and Bing amongst others.

An interesting statement in Steve's analysis is the following:

Another surprise from last week’s survey was that the mobile version of Google Search had 68 images in the results HTML document as data: URIs, compared to only 10 for desktop and iPad. Mobile browsers open fewer TCP connections and these connections are typically slower compared to desktop, so reducing the number of HTTP requests is important.

Another way of putting this is that, when it comes to serving pages for mobile devices, Google are prepared to take a hit in terms of the total amount of data served (as a data URI for a given resource is around 30% larger than the original resource) in return for a lower total number of TCP connections. Therefore, it seems we can take this as evidence that reducing the number of TCP connections more than makes up for increasing our overall data payload by 30%.

As an aside, when considering TCP connections, you should think about initially serving a large resource for a given domain followed by smaller resources, rather than the opposite. This reason for this was outlined in Hossein Lotfi's Velocity 2014 presentation, where he pointed out that serving a large resource initially will expand the slow start TCP window quicker than will serving a smaller resource.

As for the reasons for why local storage is of more interest on mobile devices than on desktop, Steve Souders provides the following reasons in another post:

  • Mobile latencies are higher and connection speeds are lower, so clientside caching is more important on mobile.
  • Mobile disk cache sizes are smaller than desktop sizes, so a better alternative is needed for mobile.
  • There are still desktop browsers with significant market share that are missing many HTML5 capabilities, whereas mobile browsers have more support for HTML5.

What size is the browser cache compared to local storage?

Browser cache can be relatively small on mobile devices, and local storage can help ensure that certain resources are available longer on the mobile device than.

Browser cache sizes are really all over the map. On devices that run Android 2.2, we only have browser cache sizes of around 5MB, while on recent iOS versions, we have browser cache sizes of more than 50MB.

This means that, on many of the lower-end devices (and on higher-end devices where the user is actively visiting many sites), content may be quickly ejected from the browser cache.

Contrast this with local storage, where we typically have 5MB available per domain fully under our control, and you'll see that we've got a good chance of ensuring that site resources persist longer in local storage than they do in browser cache.

However, given the chance that a resource may still be in browser cache, it only makes sense to prefer local storage over browser cache if it is at least as fast.

Which is faster, browser cache or local storage?

Peter McLachlan at Mobify did some tests comparing local storage and browser cache across a range of mobile operating systems, and found that local storage is typically faster, sometimes more than twice as much.

It does appear that there are differences between mobile and desktop in terms of the relative speeds of local storage and browser cache and, apparently, on desktop browser cache can be faster than local storage.

Mat Scales did some useful tests to prove this is the case (and you can repeat these on your own device), showing that local storage is often typically slower in loading than the browser cache on desktop (though Safari is the notable exception, being blazingly fast and reading and writing from local storage).

The lesson

So, the lesson here seems to be that using local storage for caching is useful on mobile, but not necessarily so on desktop.

It also appears that local storage retrieval is markedly slower on multi-process browsers, as the OS is retrieving from disk rather than memory, and multi-process browsers currently do this via a blocking call to the main process).

In this case it appears wise to ensure that each retrieval returns as much information as possible; ideally, I'd guess we'd store all of the assets for a given domain in one local storage chunk. Do note that you're typically limited to 5MB of local storage per domain, and that you'll have to handle any problems that may arise if you go over this limit.

Other factors

From the above discussion, the use of local storage for caching appears attractive; however, note that a problem arises if the cookie exists, but local storage has been cleared or compromised. In this case we need to detect the problem, and this is the approach that Google use, clearing the cookie and reloading the entire page in the process, and repopulating local storage.

Note also that the browser cache has other advantages over local storage than simply speed. When we serve a page, we can specify the Last-Modified and Expires cache headers, thus allowing the browser to know when to invalidate the resources in the browser cache.

If we are instead using local storage, we have to either build this functionality into our scripts that control the loading of resources from the local storage, or alternatively restrict ourselves to only storing long-lived resources in local storage.

Peter McLachlan's post also mentioned a few other considerations:

  • LocalStorage space is relatively small, 5MB on most browsers, and it can only store string data. This is effectively halved because localStorage stores strings as double-byte characters (UTF-16).
  • LocalStorage reads and writes do block the rendering thread. Where possible reads/writes should be done after initial page rendering is complete. You may want to avoid operations that are likely to cause performance problems such as a very large numbers of operations (thousands or tens of thousands of reads or writes), as well as read/write operations larger than 1MB.
  • Browsers should do a better job of exposing storage space used by websites taking advantage of the web storage APIs to smartphone users. Be a good neighbour and don't use localStorage recklessly, use it for key resources that are in your critical path.

Basket.js

Addy Osmani (with contributions from Sindre Sorhus, Andrée Hansson and Mat Scales) has created basket.js, which he is calling "a simple (proof-of-concept) script loader that caches scripts with localStorage".

The great thing about this project is that it looks like it gives us all the abilities that we need to manage the loading, caching, retrieval and cache invalidation of scripts into local storage.

Note that if you want to use basket.js to store non-script resources in local storage (such as CSS), you'll need to wrap them in JavaScript, which is not too difficult, though do be aware of the problems of @import statements in your CSS.

As mentioned by Andrew Wakeling:

CSS has the ability to load other resources stylesheets via @import and these calls aren't currently handled by basket.js. (A solution would probably require parsing of the CSS.)

Since the CSS is directly inserted into the DOM as a style element, relative paths are no longer relative to their source path. This is demonstrated by the difference seen in basket.html vs normal.html.

This problem is not exclusive to @import but can occur with any other resource which includes a path (e.g. url). A workaround to this problem is to always use absolute paths.

Persuing the issues for the basket.js project, it looks like they've given thought to making basket.js more fully support CSS other resources, as well as supporting other storage solutions besides local storage, though it appears progress is slow.

Going further

We can incorporate the idea of using local storage for caching resources on mobile into a wider strategy to make our pages load faster on mobile devices.

For instance, the Filament group are using an approach whereby they are shortening the critical path for loading pages.

This involves inlining the CSS needed for critical path rendering and asynchronously loading other CSS and scripts where possible to prevent blocking.

Normally, inlining CSS means that the benefits of caching are lost, as the browser doesn't know to cache the inlined CSS. The Filament group's approach here is to use a cookie:

On the first time a browser visits this site we set a cookie after asynchronously requesting certain files (such as our site’s full CSS file) to specify that now that the files have been requested, and are likely to be cached by the browser. Then, on subsequent visits to our site, our server-side code checks if that cookie is present and if so, it avoids including any inline CSS and instead just references the full CSS externally with an ordinary link element. This seems to make the page load a little more cleanly on return visits. We do the same for our fonts and icons CSS files as well.

They've taken this concept further, creating a library called enhance.js which can be used to apply these enhancements progressively, depdending on whether the browser cuts the mustard.

We can adapt the Filament Group's approach such that, instead of just retrieving resources asynchronously from remote URLs, we instead add a local storage caching layer using something like basket.js.

With the combination of the two approaches, we end up with a resource loading strategy whereby:

  • CSS resources on the critical path are inlined, therefore not incurring an extra HTTP request
  • scripts that we need before render start (such as HTML5shiv.js) are loaded synchronously
  • we asynchronously load those scripts and CSS resources not on the critical path, storing them in local storage. Note that this includes the full CSS, i.e. including the CSS that was inlined in the page
  • we set a cookie indicating that these resources are now cached in local storage
  • on the first request these asynchronous resources are loaded from remote URLs
  • on subsequent requests these asynchronous resources are loaded from local storage
  • ideally, we'd avoid serving the inlined CSS more than once, by using server side includes (as the filament group do, with their enhance.js project)

Using the above technique (sans the local storage caching), the Filament Group managed to acheive start render times of around 300ms, as opposed to around 900ms without deferring resources to be loaded asynchronously.

Putting together inlined critical path CSS and asynchronous / lazy loading of non critical assets with the use of local storage for caching seems like a good potential solution for fast rendering on mobile devices on the initial page load, and constantly fast subsequent page reloads.

I'd be interested in hearing from anyone who's given this a go in practice.