Offline Strategies for PWA

person carlosrojasofolder_openpwaaccess_time November 10, 2017

Currently, most Web Apps run without offline capabilities, that means, they depend almost entirely on the server. We can feel this when we are disconnected from our browser in an App with responsive design and we receive the “downsaur” which is the way in which Google Chrome tells us that we are offline.

This is not the only scenario that we must avoid is also the common scenario of a very unstable and almost non-existent connection that it’s called LiFi which show us nothing neither our App nor the dinosaur but leaves us waiting with the browser window blank. Remember that?

When to store the information?

If you remember the life cycle of a service worker after is registered, the Service Worker begins to go through a series of events that we can use to carry out our Caching strategy

The “install” event is the best time to do our initial storage of our files, to achieve this we can do something like this.

sw.js

var CACHENAME = "cachestore";
var FILES = [
  "/index.html",
  "/css/style.css",
  "/js/app.js"
];

self.addEventListener("install", function(event) {
  event.waitUntil(
    caches.open(CACHENAME).then(function(cache) {
      return cache.addAll(FILES);
    })
  );
}); 

As you can see above we have our files to store in “FILES” array that would be our Shell, then we enter “install” event which brings the elements of the APP Shell from the network and stores them in the Cache Storage giving way to “activated” event.

When to update our Cache Storage files?

Although it is assumed that the files that we have in our App Shell are not going to change frequently, they will do so in the future. For example An improvement of UX, a change of branding, etc. That is why we must prepare for this moment and not leave our users with version 1 of our App forever.

To manage the update of the Cache Storage we will use a version number in the name of our Cache and the “activated” event of the Service Worker life cycle to perform the deletion of the old files and change them for the new ones. By updating our Service Worker a bit, we would have something like this:

sw.js

var CACHENAME = "cachestore-v1";
var FILES = [
  "/index.html",
  "/css/style.css",
  "/js/app.js"
];

self.addEventListener("install", function(event) {
  event.waitUntil(
    caches.open(CACHENAME).then(function(cache) {
      return cache.addAll(FILES);
    })
  );
}); 

self.addEventListener('activate', function(event) {
  var version = 'v1';
  event.waitUntil(
    caches.keys()
      .then(cacheNames =>
        Promise.all(
          cacheNames
            .map(c => c.split('-'))
            .filter(c => c[0] === 'cachestore')
            .filter(c => c[1] !== version)
            .map(c => caches.delete(c.join('-')))
        )
      )
  );
});

If you look at the code above you will see that we have added a series of operations when our Service Worker arrives at our ‘activate’ event, what we do is verify if the name that it has in CACHENAME has the same version number as the one we have in version if not we erase that other cache we have detected. This will ensure that previous versions such as cachestore-v0 are removed from Cache Storage on the client.

Now you have your files in the Cache Storage and you can update it below. We will see how to respond to the requests of our users.



How to respond to Requests.

Although there are different mixes of strategies to respond to the requests of your users using the Cache, some patterns have been identified that work well in most of the scenarios, these are:

cacheFirst

This pattern responds to requests with files from the Cache Storage. If the response fails from the Cache Storage try to respond with the files from the Network.

sw.js

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

cacheOnly

This pattern responds to all requests with Cache Storage files. If doesn’t find it, it will fail.

sw.js


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

networkFirst

This pattern responds to all requests with the content of the network. If it fails it tries to respond with the contents of the Cache Storage.

sw.js


self.addEventListener("fetch", function(event) {
  event.respondWith(
    fetch(event.request).catch(function() {
      return caches.match(event.request);
    })
  );
});

networkOnly

This pattern is basically how the apps work currently. Everything is searched on the network and presented to the user. Think of this strategy when there are requests that you are not going to store in the Cache Storage.

sw.js


self.addEventListener("fetch", function(event) {
  event.respondWith(
    fetch(event.request)
  );
});

generic.

This pattern responds when you can get a file neither from the Cache Storage nor from the network, then, respond with something generic from the Cache Storage.

sw.js


self.addEventListener("fetch", function(event) {
  event.respondWith(
    fetch(event.request).catch(function() {
      return caches.match(event.request).then(function(response) {
        return response || caches.match("/404.html");
      });
    })
  );
});

If you want to learn a little more, go to serviceworke.rs, which is an excellent source of recipes for our Service Worker.

See you…

Carlos Rojas

Liked it? Take a second to support carlosrojaso on Patreon!