Making PWAs work offline with Service workers

Service Workers are a virtual proxy between the browser and the network. They finally fix issues that front-end developers have struggled with for years — most notably how to properly cache the assets of a website and make them available when the user’s device is offline.

Security

Because they are so powerful, Service Workers can only be executed in secure contexts (meaning HTTPS). If you want to experiment first before pushing your code to production, you can always test on a localhost or setup GitHub Pages — both support HTTPS.

Offline First

The “offline first” — or “cache first” — pattern is the most popular strategy for serving content to the user. If a resource is cached and available offline, return it first before trying to download it from the server. If it isn’t in the cache already, download it and cache it for future usage.

“Progressive” in PWA

When implemented properly as a progressive enhancement, service workers can benefit users who have modern browsers that support the API by providing offline support, but won’t break anything for those using legacy browsers.

Service workers in the js13kPWA app

Enough theory — let’s see some source code!

Registering the Service Worker

We’ll start by looking at the code that registers a new Service Worker, in the app.js file:

if('serviceWorker' in navigator) {
navigator.serviceWorker.register('./pwa-examples/js13kpwa/sw.js');
};

Lifecycle of a Service Worker

Installation

The API allows us to add event listeners for key events we are interested in — the first one is the install event:

self.addEventListener('install', (e) => {
console.log('[Service Worker] Install');
});
var cacheName = 'js13kPWA-v1';
var appShellFiles = [
'/pwa-examples/js13kpwa/',
'/pwa-examples/js13kpwa/index.html',
'/pwa-examples/js13kpwa/app.js',
'/pwa-examples/js13kpwa/style.css',
'/pwa-examples/js13kpwa/fonts/graduate.eot',
'/pwa-examples/js13kpwa/fonts/graduate.ttf',
'/pwa-examples/js13kpwa/fonts/graduate.woff',
'/pwa-examples/js13kpwa/favicon.ico',
'/pwa-examples/js13kpwa/img/js13kgames.png',
'/pwa-examples/js13kpwa/img/bg.png',
'/pwa-examples/js13kpwa/icons/icon-32.png',
'/pwa-examples/js13kpwa/icons/icon-64.png',
'/pwa-examples/js13kpwa/icons/icon-96.png',
'/pwa-examples/js13kpwa/icons/icon-128.png',
'/pwa-examples/js13kpwa/icons/icon-168.png',
'/pwa-examples/js13kpwa/icons/icon-192.png',
'/pwa-examples/js13kpwa/icons/icon-256.png',
'/pwa-examples/js13kpwa/icons/icon-512.png'
];
var gamesImages = [];
for(var i=0; i<games.length; i++) {
gamesImages.push('data/img/'+games[i].slug+'.jpg');
}
var contentToCache = appShellFiles.concat(gamesImages);
self.addEventListener('install', (e) => {
console.log('[Service Worker] Install');
e.waitUntil(
caches.open(cacheName).then((cache) => {
console.log('[Service Worker] Caching all: app shell and content');
return cache.addAll(contentToCache);
})
);
});

Activation

There is also an activate event, which is used in the same way as install. This event is usually used to delete any files that are no longer necessary and clean up after the app in general. We don't need to do that in our app, so we'll skip it.

Responding to fetches

We also have a fetch event at our disposal, which fires every time an HTTP request is fired off from our app. This is very useful, as it allows us to intercept requests and respond to them with custom responses. Here is a simple usage example:

self.addEventListener('fetch', (e) => {
console.log('[Service Worker] Fetched resource '+e.request.url);
});
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then((r) => {
console.log('[Service Worker] Fetching resource: '+e.request.url);
return r || fetch(e.request).then((response) => {
return caches.open(cacheName).then((cache) => {
console.log('[Service Worker] Caching new resource: '+e.request.url);
cache.put(e.request, response.clone());
return response;
});
});
})
);
});

Updates

There is still one point to cover: how do you upgrade a Service Worker when a new version of the app containing new assets is available? The version number in the cache name is key to this:

var cacheName = 'js13kPWA-v1';
contentToCache.push('/pwa-examples/js13kpwa/icons/icon-32.png');// ...self.addEventListener('install', (e) => {
e.waitUntil(
caches.open('js13kPWA-v2').then((cache) => {
return cache.addAll(contentToCache);
})
);
});

Clearing the cache

Remember the activate event we skipped? It can be used to clear out the old cache we don't need anymore:

self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then((keyList) => {
return Promise.all(keyList.map((key) => {
if(key !== cacheName) {
return caches.delete(key);
}
}));
})
);
});

Other use cases

Serving files from cache is not the only feature Service Worker offers. If you have heavy calculations to do, you can offload them from the main thread and do them in the worker, and receive results as soon as they are available. Performance-wise, you can prefetch resources that are not needed right now, but might be in the near future, so the app will be faster when you actually need those resources.

Summary

In this article we took a simple look at how you can make your PWA work offline with service workers.Learn more about the concepts behind the Service Worker API and how to use it in more detail.

Software Developer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store