Ionic 3 with Workbox Service Worker

Published: November 05, 2017  •  Updated: April 22, 2018  •  pwa, ionic3, javascript

The description in this tutorial does not work when your application loads modules lazily. Read this blog post instead: https://golb.hplar.ch/2017/12/Workbox-in-Ionic-and-Lazy-Loading-Modules.html

This tutorial also does not apply to Ionic 4 applications. Read this blog post instead: https://golb.hplar.ch/2018/06/workbox-serviceworker-in-angular-project.html


When you want to add a Service Worker to your web application, to make your site work offline or improve loading time on repeat-visits, you can either write it yourself from scratch or use a Service Worker library. Writing a Service Worker yourself is not the easiest task and most projects build their code with one of the libraries. It makes implementing the Service Worker easier and less error prone and you don't have to think about all the possible edge cases.

Popular libraries are sw-precache and sw-toolbox. At Google IO 2017 in May Google announced a new Service Worker library: Workbox. According to the documentation, Workbox is the successor to the aforementioned two libraries. After the first release in May 2017, Google released version 2 in August 2017 and just released version 3 in March 2018.

In this blog post I show you how you can integrate Workbox 3 into the build process of an Ionic app and generate a Service Worker.

Let's start with an empty application

ionic start myapp blank

When you create a new Ionic application with ionic start you get a sw-toolbox Service Worker for free. This service worker is by default disabled and you have to uncomment the code in the src/index.html file. Before we add Workbox we delete the sw-toolbox library and while we are at it also uninstall the ionic-native packages that we don't need for a web application. We also no longer need the resource folder that hosts assets for the Cordova build.

npm uninstall sw-toolbox
npm uninstall @ionic-native/core
npm uninstall @ionic-native/splash-screen
npm uninstall @ionic-native/status-bar
rm -fr resources

Also remove the ionic native code from src/app/app.modules.ts and src/app/app.component.ts.

Next install the Workbox library and command line interface.

npm install workbox-cli -D
npm install workbox-sw	

Open src/index.html and remove the script tag that imports cordova.js

<script src="cordova.js"></script>

and uncomment the code that installs the Service Worker. This is the code that ionic start generated for the sw-toolbox library, but the code works with the Workbox Service Worker too.

<script>
 if ('serviceWorker' in navigator) {
   navigator.serviceWorker.register('service-worker.js')
     .then(() => console.log('service worker installed'))
     .catch(err => console.error('Error', err));
 }
</script>

src/index.html

Next we have to create a custom copy configuration, that copies the Workbox library to the build folder (www). The Service Worker that we will create depends on this library. Create a file copy.config.js and add this code:

module.exports = {
  copyWorkbox: {
	src: ['./node_modules/workbox-sw/build/workbox-sw.js',
		    './node_modules/workbox-core/build/workbox-core.prod.js',
		    './node_modules/workbox-precaching/build/workbox-precaching.prod.js'],
	dest: '{{WWW}}/workbox-3.6.2'
  }
}

copy.config.js

In my project I store this configuration file in the root of the project, but you can place it in any directory you like.

Now insert an ionic_copy configuration in the package.json file. This tells the Ionic build script to read our custom copy configuration and execute it together with the standard copy step.

"config": {
 "ionic_copy": "./copy.config.js"
},

package.json

Next we replace the existing sw-toolbox Service Worker code with the Workbox code. Open src/service-worker.js and overwrite it with this code

importScripts('workbox-3.6.2/workbox-sw.js');
workbox.setConfig({
  debug: false,
  modulePathPrefix: 'workbox-3.6.2/'
});
workbox.skipWaiting();
workbox.clientsClaim();
workbox.precaching.precacheAndRoute([]);

src/service-worker.js

This is the minimal code you need for a Workbox Service Worker. Import the library, configure it and call the precacheAndRoute method. precacheAndRoute() takes an array of resources, fetches and stores them into the cache and installs a route to these resources. Next time the browser requests these files the Service Worker serves them from the cache. Note that the array is empty, we don't have to manually list all the resources we want to cache, the Workbox CLI takes care of this and injects the necessary code into this template.

You can insert multiple precacheAndRoute calls, like I did in this example. But just one call has to contain the argument [] for the CLI to find the injection point.

workbox.precaching.precacheAndRoute([]);
workbox.precaching.precacheAndRoute([{
  "url": "https://static.rasc.ch/Workbox-Logo-Grey.svg"
}]);

In this example we also cache the SVG and install a route, so it will be available when the device is offline.


Next we add the build step that injects the code to the package.json file.

"postbuild": "workbox injectManifest",

package.json

We name the task postbuild so npm will automatically run it after the build task (npm run build).

Before we start the build process we have to create a configuration file for the Workbox CLI. Create a new file workbox-config.js in the root of the project and add this code.

module.exports = {
  "globDirectory": "www/",
  "globPatterns": [
    "assets/fonts/*.woff2",
    "build/**/*.css",
	  "build/**/*.js",
    "index.html",
    "manifest.json"
  ],
  "dontCacheBustUrlsMatching": new RegExp('.+\.[a-f0-9]{8}\..+'),
  "maximumFileSizeToCacheInBytes": 5000000,
  "swSrc": "src/service-worker.js",
  "swDest": "www/service-worker.js"
};

workbox-config.js

globDirectory specifies the base directory where the resources are located we want to cache. globPatterns lists all the files we want to cache with the precache method. swSrc points to the Service Worker template we created earlier and swDest specifies the location where the Workbox CLI stores the modified file.

When the build process executes the command workbox injectManifest, it first reads the configuration from workbox-config.js, searches for all the resources in globDirectory that matches the pattern globPatterns, reads the src/service-worker.js file, adds an entry for each found resource to the precacheAndRoute([{...},{...}, ...]) code and stores the changed file in www/service-worker.js.

Instead of injection the resources into the service-worker.js template, you can tell the Workbox CLI to create a Service Worker from scratch with the workbox generateSW command. When you go this way you no longer need the src/service-worker.js file and you can remove the swSrc option from the workbox-config.js file.
The drawback of this solution is that you can't add custom code to the Service Worker.

After these preparations, we can start the build with npm run build.

Workbox might complain about the file size of the vendor.js file. When we run a non optimized Ionic build, the vendor.js has a size of over 4MB. The Workbox generator ignores by default files that are bigger than 2,097,152 Bytes. If you want to cache files that are bigger, you have to add the following configuration option to the workbox-config.js file

"maximumFileSizeToCacheInBytes": 5000000,

This setting allows files up to 5MB to be stored in the cache.

After the build successfully finished, check that the build process copied the three Workbox files into the www/workbox-3.0.0 folder and open the service-worker.js file with an text editor. Check the code that the injectManifest step added to the Service Worker. If the array is empty or files are missing check and adjust the globDirectory and globPatterns configuration options in workbox-config.js.

The generated service worker file (www/service-worker.js) should look like this.

importScripts('workbox-3.6.2/workbox-sw.js');
workbox.setConfig({
  debug: false,
  modulePathPrefix: 'workbox-3.6.2/'
});
workbox.skipWaiting();
workbox.clientsClaim();
workbox.precaching.precacheAndRoute([
  {
    "url": "assets/fonts/ionicons.woff2",
    "revision": "311d81961c5880647fec7eaca1221b2a"
  },
  {
    "url": "assets/fonts/roboto-bold.woff2",
    "revision": "28d80f43ae4cc35f19e1f1a6ab670f25"
  },
  ....
]);

When the browser loads your application it first parses and executes code he finds in index.html, installs and executes service-worker.js and the Service Worker fetches the listed resources and stores them in the cache.

What is the purpose of the revision field? To understand that you need to know that using fetch and the Cache API in the Service Worker does not bypass the http cache of the browser. When the Service Worker fetches a resource the browser first looks for the file in the http cache and returns it from there if it exists. This could be a problem when your web server serves assets with a cache control header and an expiration date far in the future. For this reason the Workbox library adds the revision hash to the URL when it fetches resources from the server. A GET request for the build/vendor.js would look like this

https://..../build/vendor.js?_workbox-precaching=11885441e3852cb6dd187eb966f64b1f

The _workbox-precaching request parameter is also called a cache buster because it forces the browser to load the newest version of the file from the server when the file is not already stored in the http cache with exactly the same URL.

In newer browsers you no longer see the _workbox-precaching query parameter. The workbox library uses instead the cache property in the Request object and set it to the value 'reload'. This value tells the browser to skip the http cache and fetch the resource from the server and then store the response in the cache.

Each time the file content changes, the revision in the service-worker.js changes. This triggers a new installation of the Service Worker code and a request for the file with the new revision hash.

When you use ionic serve during development the browser loads the service-worker.js file from the src directory which does not cache anything (workbox.precaching.precacheAndRoute([]);), which I like because then nothing gets cached and I don't have to worry about the Service Worker interfering with the application.

But that's a bit of a problem when you want to test the generated Service Worker. Just opening the index.html file does not work because the browser does not install a Service Worker when you serve the site from the file:// protocol. You have to serve the site over the http protocol. You could copy the www folder to a web server or add a web server to the project.

For this project I add the http-server npm package.

npm install http-server -D

and add a task

"open": "http-server www -o -a localhost -p 1234"

package.json

Now we can simply type npm run open and node starts a http server, opens the browser window and serves the content from the www folder. You can also combine the build step with the open task:
npm run build && npm run open
or start a production build with
npm run build --prod && npm run open

Note that browsers only install Service Workers when they are served over a TLS connection. Exceptions are http connections from localhost or 127.0.0, they don't need to be encrypted.


More work

Unfortunately the previous setup has a flaw. When you open the application the first time with an empty cache, the browser parses the index.html page, requests all the referenced resources from the server and stores them in the http cache, installs the Service Worker and execute it. Now the Service Worker wants to cache all the resources and because he is using a cache busting URL or the Request.cache='reload' option for fetching the resources, the requests bypass the http cache and all files are sent twice over the network.

To solve that we need to remove the revision field from the precache option. Without this option, Workbox requests all the resources the normal way and first looks in the http cache.

But with this setup it would very difficult to update the application. For instance Workbox stores the file build/vendor.js under that name in the Cache and as soon as a resource is pre cached Workbox uses the cache first strategy and never request it from the server again. When we update the application the reference in index.html is still build/vendor.js and the Service Worker serves the old file from the cache.

A solution is to rename the files each time we update the application. In our example we add a hash, calculated from the content of the file, to the file name. This has the advantage that the name only changes when the content changes.

First we install three additional packages.

npm install hashmark -D
npm install map-replace -D  
npm install shx -D

hashmark is responsible for calculating the hash from the file content and renaming the file.
map-replace replaces the references to these files (for instance link and script tags in html pages).
shx is a wrapper around ShellJS Unix commands, providing an easy solution for simple Unix-like, cross-platform commands in npm package scripts.

Then we add these two tasks to our package.json

"hashcb": "hashmark -l 8 -r --cwd www/build \"*.{js,css}\" {name}.{hash}{ext} | map-replace -m \"<[^>]+>\" www/index.html",
"dist": "npm run clean && shx rm -rf www/* && npm run build --prod && npm run hashcb && workbox injectManifest"

package.json

The dist task is an aggregation of several other tasks, first it cleans the output directory, starts an Ionic production build, runs hashcb and injects the resources into the service worker file. Make sure that injectManifest runs after the filename hashing process, because the service-worker.js needs to reference the hashed file names.

The hashcb tasks executes hashmark. The option -l 8 creates hash codes that are 8 bytes long, -r replaces the original file with the renamed file. -cwd www/build specifies the base directory where hashmark should look for files. *.{js,css} specifies the files it should process, in this case only files with a postfix js and css. The last parameter specifies the pattern for the new file name. With this configuration hashmark renames build/vendor.js to build/vendor.f2e3575e.js.

hashmark writes by default a JSON file to the standard output that looks like this

{"main.js":"main.cace105e.js","polyfills.js":"polyfills.a3d47f9d.js",
 "main.css":"main.62012289.css","vendor.js":"vendor.2a4333ee.js"}

The key is the original file name and the value contains the new hashed file name. map-replace is able to read this file and replaces all the occurrences of the original file name with the new hashed filename.

For this application we only have to check the index.html file. When the hashcb task finishes without any errors, you can check the index.html file. All the script and link tags should reference the hashed names.

<script src="build/vendor.f2e3575e.js"></script>

With this change in place we can serve the assets with a cache control header and a long expiration duration. I often configure the web server to add a cache control header with an expiration date of one year in the future.

Cache-Control: public, max-age=31536000

There is one last configuration step we need to do. The Workbox CLI still generates the revision field.

workbox.precaching.precacheAndRoute([
  {
    "url": "build/main.39fbcc18.js",
    "revision": "ba76e4e1923eb5c0bbeca9028796862e"
  },

To suppress the revision field we need to add the dontCacheBustUrlsMatching option to the workbox-config.js file and specify a regular expression. This regular expression checks if the name contains an 8 byte hash.

"dontCacheBustUrlsMatching": new RegExp('.+\.[a-f0-9]{8}\..+'),

workbox-config.js

For all the resources, where the name matches the regular expression, the Workbox CLI does not add the revision field.

  {
    "url": "build/main.39fbcc18.js"
  },

With this setup in place, the browser first parses the index.html page, requests all the referenced resources and stores them in the http cache, then it installs and executes the Service Worker and requests the resources again. Because Workbox requests resources without the revision field with a normal request, the browser first checks the http cache and because all resources are already loaded from index.html no additional request is sent to the server. The Service Worker then stores the resources in the cache storage and from this point in time are served from there. The next update changes the filesnames and the cycle begins from the beginning.

Note that this only works when the server sends these resources with a cache control header that commands the browser to store the resources in the http cache. If the server sends the resources with a no-store/no-cache Cache-Control header, the browser would still request the resources twice, because the responses from the index.html requests are not stored in the http cache.


This setup only removes the revision field from resources that change their name during an update. The name of other files, like index.html, cannot be changed and it's important that these resources have a revision value assigned to it.

Workbox stores all the urls and revisions from the precache configuration in IndexedDB. Each time the browser executes the service-worker.js code, Workbox compares the revision with the stored value and if it's the same returns the cached content from the Cache API.
If it's different, for instance after an update, Workbox requests the resource from the server with the cache busting url or the Request.cache='reload' option, stores the response in the cache and updates the revision value in IndexedDB. Until next time the revision value in service-worker.js changes, Workbox always serves the content from the Cache API.


You find the complete source code for this project on GitHub:
https://github.com/ralscha/blog/tree/master/workbox