Live Data in the Service Worker

Introduction

Offline support and reliable performance are key features of Progressive Web Apps. This text describes some recommendations for storing different kinds of data in a PWA.

Where should offline data be stored?

A general guideline for data storage is that URL addressable resources should be stored with the Cache interface, and other data should be stored with IndexedDB. For example HTML, CSS, and JS files should be stored in the cache, while JSON data should be stored in IndexedDB. Note that this is only a guideline, not a firm rule.

Why IndexedDB and the Cache interface?

There are a variety of reasons to use IndexedDB and the Cache interface. Both are asynchronous and accessible in service workers, web workers, and the window interface. IndexedDB is widely supported, and the Cache interface is supported in Chrome, Firefox, Opera, and Samsung Internet.

In this text we use Jake Archibald's IndexedDB Promised library, which enables promise syntax for IndexedDB. There are also other IndexedDB libraries that can be used to abstract some of the less convenient aspects of the API.

Debugging support for IndexedDB is available in Chrome, Opera, Firefox and Safari. Debugging support for Cache Storage is available in Chrome, Opera, and Firefox. These are covered in Tools for PWA Developers.

How Much Can You Store

Different browsers allow different amounts of offline storage. This table summarizes storage limits for major browsers:

Browser

Limitation

Notes

Chrome, Opera, and Samsung Internet

Up to a quota. Check usage with the Quota API

Storage is per origin not per API (local storage, session storage, service worker cache and IndexedDB all share the same space)

Firefox

No limit

Prompts after 50 MB of data is stored

Mobile Safari

50MB

 

Desktop Safari

No limit

Prompts after 5MB of data is stored

Internet Explorer (10+)

250MB

Prompts after 10MB of data is stored

Using IndexedDB and the Cache interface

Storing data with IndexedDB

IndexedDB is a noSQL database. IndexedDB data is stored as key-value pairs in object stores. The table below shows an example of an object store, in this case containing beverage items:

#

Key (keypath 'id')

Value

0

1234

{id: 123, name: 'coke', price: 10.99, quantity: 200}

1

9876

{id: 321, name: 'pepsi', price: 8.99, quantity: 100}

2

4567

{id: 222, name: 'water', price: 11.99, quantity: 300}

The data is organized by a keypath, which in this case is the item's id property. You can learn more about IndexedDB in the corresponding text, or in the code lab.

The following function could be used to create an IndexedDB object store like the example above:

service-worker.js

function createDB() {
  idb.open('products', 1, function(upgradeDB) {
    var store = upgradeDB.createObjectStore('beverages', {
      keyPath: 'id'
    });
    store.put({id: 123, name: 'coke', price: 10.99, quantity: 200});
    store.put({id: 321, name: 'pepsi', price: 8.99, quantity: 100});
    store.put({id: 222, name: 'water', price: 11.99, quantity: 300});
  });
}

Here we create a 'products' database, version 1. Inside the 'products' database, we create a 'beverages' object store. This holds all of the beverage objects. The beverages object store has a keypath of id. This means that the objects in this store will be organized and accessed by the id property of the beverage objects. Finally, we add some example beverages to the object store.

The service worker activation event is a good time to create a database. Creating a database during the activation event means that it will only be created (or opened, if it already exists) when a new service worker takes over, rather than each time the app runs (which is inefficient). It's also likely better than using the service worker's installation event, since the old service worker will still be in control at that point, and there could be conflicts if a new database is mixed with an old service worker. The following code (in the service worker file) could be used to create the database shown earlier on service worker activation:

service-worker.js

self.addEventListener('activate', function(event) {
  event.waitUntil(
    createDB()
  );
});

Once an IndexedDB database is created, data can then be read locally from IndexedDB rather than making network requests to a backend database. The following code could be used to retrieve data from the example database above:

service-worker.js

function readDB() {
  idb.open('products', 1).then(function(db) {
    var tx = db.transaction(['beverages'], 'readonly');
    var store = tx.objectStore('beverages');
    return store.getAll();
  }).then(function(items) {
    // Use beverage data
  });
}

Here we open the products database and create a new transaction on the beverages store of type readonly (we don't need to write data). We then access the store, and retrieve all of the items. These items can then be used to update the UI or perform whatever action is needed.

Storing assets in the Cache interface

URL addressable resources are comparatively simple to store with the Cache interface. The following code shows an example of caching multiple resources:

service-worker.js

function cacheAssets() {
  return caches.open('cache-v1')
  .then(function(cache) {
    return cache.addAll([
      '.',
      'index.html',
      'styles/main.css',
      'js/offline.js',
      'img/coke.jpg'
    ]);
  });
}

This code opens a cache-v1 cache, and stores index.html, main.css, offline.js, and coke.jpg.

The service worker installation event is a good time to cache static assets like these. This ensures that all the resources a service worker is expected to have are cached when the service worker is installed. The following code (in the service worker file) could be used to cache these types of files during the service worker install event:

service-worker.js

self.addEventListener('install', function(event) {
  event.waitUntil(
    cacheAssets()
  );
});

Once assets are cached, they can be retrieved during fetch events. The following code (in the service worker file) allows resources to be fetched from the cache instead of the network:

service-worker.js

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      // Check cache but fall back to network
      return response || fetch(event.request);
    })
  );
});

This code adds a fetch listener on the service worker that attempts to get resources from the cache before going to the network. If the resource isn't found in the cache, a regular network request is still made.

Further reading