Caching is a great way to improve performance. We can store data, assets and whole pages. There are different technologies to cache and each has its benefits. In this article, I will go through different methods and when and how to use them.

Cookies

The first visit, the first impression a user has from a site is very important. Performance is really important here – 53% of mobile users abandon sites that take longer than 3 seconds to load – and you should do everything to deliver fast.

One way to get a fast load on first visit is to only load critical parts. Cookies are a good choice here, as you can access them on the client-side and on the server-side. This way you can save a cookie on first visit with JavaScript:

document.cookie = "has_visited=true";

and check for the cookie on the server-side (in this case PHP):

<?php if ( isset($_COOKIE['has_visited']) ) { ?>
    <!-- user has visited before, load non-critical stuff -->
<? } ?>

to load non-critical stuff for returning visitors.

Examples

You can use them for example in combination with Critical CSS., Serve the Critical CSS if the cookie does not exist and load the rest of the CSS asynchronous and save a cookie. Otherwise, load the full cached CSS instead of the Critical CSS. This way the site loads fast on first visit and you don’t have to load the uncached critical CSS for returning visitors saving them some kB.

Here is an example of Critical CSS using a common theme as a base.

You can also use cookies to decide if you want to use web font or not. On first visit you can use system fonts and cache web font for later. On returning visit you can check if the cookie exists and use the web font straight away.

Or you could load non-critical content (Most read articles, Social Shares…) only if user has already visited, cookie is set and you have enough resources cached to load these extra bytes.

Summary

Use Cookies if you want to access them on the client-side and server-side. Great for checking if a visitor has visited before and to decide if you can load non-critical resources.

Issues

One issue with Cookies is that some users have blocked them or delete them after every visit, mostly because of privacy. We can’t and shouldn’t do anything about this, we should respect their decision even if these means we deliver them our “first-visit experience” every time.

Also note that it is synchronous and therefore can’t be used in a Web Worker or Service Worker, but this may change in the future as there is an Async Cookies API sketched out at the moment.

Furthermore cookie data is added to every HTTP request header, so keep it to a minimum.

Browser support

Cookies are supported in every browser as well in any server-side language.

Local Storage

The next way to improve performance is using localStorage. With localStorage you can store data and assets to serve them for returning visitors avoiding HTTP-requests.

Examples

You can use localStorage to cache JavaScript or CSS files more reliable than with the HTTP cache. For example, you can store the CSS with the web font in localStorage and serve it from there for returning visitors.

html {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}

.font-loaded {
  font-family: 'MyFont', sans-serif;
}
(function () {
  'use strict';
  var css_href = 'fonts.css';
  var localStorageSupported = function() {
    try {
      localStorage.setItem('test', 'test');
      localStorage.removeItem('test');
      return true;
    } catch(e) {
      return false;
    }
  }

  if (localStorageSupported() && localStorage.webFont) {
    injectRawStyle(localStorage.getItem('webFont'));
  } else {
    window.onload = function() {
      injectFontsStylesheet();
    }
  }

  function injectFontsStylesheet() {
    var xhr = new XMLHttpRequest();
      xhr.open('GET', css_href, true);
      xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
          injectRawStyle(xhr.responseText);
          localStorage.setItem('webFont', xhr.responseText);
        }
      }
    xhr.send();
  }

  function injectRawStyle(text) {
    var style = document.createElement('style');
    style.innerHTML = text;
    document.getElementsByTagName('head')[0].appendChild(style);
    document.documentElement.className = 'font-loaded';
  }
}());

We can combine this with cookies to set the font-loaded class directly if the Cookie is set

php
<?php if ( isset($_COOKIE['font_loaded']) ) { ?>
<html class="font-loaded">
<? } else ?>
<html>
<? } ?>

to use the web font straight away.

Summary

Use localStorage to reliable save assets for returning visitors. It can have great performance impacts, when using with web fonts, but be sure to test against other strategies

Issues

In private browsing mode, some browsers (Safari, Android browser…) do not support setting localStorage and doing so will throw an exception. Therefore, you should always use try…catch when using localStorage.

var hasLocalStorage = function() {
    try {
        localStorage.setItem(mod, mod);
        localStorage.removeItem(mod);
        return true;
    } catch (exception) {
        return false;
    }
};

Reading from and writing to localStorage happens synchronous and can be slow, so make sure to test on real devices.

Browser support

Browser support for localStorage is very good except in proxy browsers, and very old browsers like Internet Explorer 7.

IndexedDB

To store complex data sets, indexedDB is a good choice. It is a transactional, non-relational storage mechanism that saves key-value pairs in Object Stores and allows searching data using indexes. This way you can store whole articles or other data sets.

Examples

You can use indexedDB to show locally-saved data on page load and then fetch new data. If the network allows it you can than show the updated data, otherwise old data will be shown and you can add a hint indicating that new data is not available at the moment because of the network connection. This way you can get an “instant-loading” feel for returning visitors.

Summary

Use indexedDB to save more complex data for returning visits. There are a lot of different libraries available, which makes it easier working with indexedDB and which also solve some browser issues.

As indexedDB is asynchronous you can use it in Web Worker and Service Worker. Great for data you want to access on the client and in the worker.

Issues

As with all other local storage the user agent may clear data at some point to free up space if storage on the local machine is running tight. To work around this, a persistent storage API is worked out at the moment to store data persistent. It is currently available as a origin trail in Chrome > 52.

When loading critical data with JavaScript always make sure it can also be accessed when the JavaScript for whatever reason fails to succeed.

Browser support

Browser support for IndexedDB is pretty good and as we are using it as an enhancement we can safely use it to speed up page load for returning visitors.

Cache API

Last but not least there is the Cache API that is part of Service Workers and lets you control the Cache.

Examples

We can use the Cache API to cache assets we need on every page (CSS, JavaScript, Logo) and serve them from the cache for returning visits.

const VERSION = '1.0.0';
const STATIC_CACHE = 'static' + VERSION;

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(STATIC_CACHE).then(function(cache) {
      return cache.addAll([
        './',
        './index.html',
        './dist/css/main.min.css',
        './dist/js/main.min.js',
        './dist/img/logo.svg'
      ]);
    })
  );
});

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      return response || fetch(event.request);
    })
  );
});

self.addEventListener('activate', function(event) {
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.filter(function(cacheName) {
          return cacheName != STATIC_CACHE;
        }).map(function(cacheName) {
          return caches.delete(cacheName);
        })
      );
    })
  );
});

Here, we first define a version so we can later update the assets in the cache and delete the old one. In the install event we cache all assets we use on all our pages. Next, in the fetch event we check if the requested assets are already in cache and return from there and otherwise request them from the network. Finally, in the activate event we clear the old cache do clear up space.

Summary

The Cache API is great for caching URL addressable resources to make them available offline. But, even more important is that you can improve page load by returning already cached assets instead of requesting them from the network.

Issues

It should be noted that to use Service Workers and the Cache API your site has to be served over a secure origin (Https) but this should be done in any case nowadays.

Browser support

Browser support for Service Workers (including the Cache API) isn’t ideal at the moment. It is currently only supported in Firefox, Opera and Chrome, but support will increase in the near future as Edge started working on it and also Safari vaguely showed positive interest. As Service Workers are progressive enhance-able per default support isn’t a big issue; While it speeds up load time for some users it doesn’t break things for all others.

Conclusion

There are many ways to improve page load for new and returning visitors. As always you should test on real devices using different network connections (Cable, LTE, 3G). This way you get a good feeling how fast your site is, what non-critical resources really should be loaded on first visit and how the page gets loaded for returning visitors.

ABOUT THE AUTHOR

Michael Scharnagl (@justmarkup) is a freelance front-end developer. He loves learning new things and finding techniques that challenge what we think is best practice, all with progressive enhancement in mind. You can find his writing on his blog, and follow him on Twitter @justmarkup.