Workbox is the workhorse for progressive web apps of today. With only a few lines of code we get precaching, runtime caching and offline mode. In this post I’ll show how to add Workbox to an existing PWA built with @vue/cli and the pwa plugin to obtain a perfect score of 100 in Google’s Lighthouse tool.
Our current project is using the @vue/cli pwa plugin and already has a service worker. As outlined in the previous post on push notifications, it is the service worker that enables us to send push notifications. However, no preaching, runtime caching or offline mode has been implemented yet. Although it is possible to write all the code for these functionalities in the service worker yourself, it is difficult, tedious and error prone. That’s exactly why Google created Workbox: as a wrapper around all the difficult bits and pieces involved when working with service workers.
But adding Workbox to my project was a bumpy road. With lots of googling, following tutorials and reading I finally created a working example and now I am ready to explain how all this ties together.
So, today we are going to extent the version that we have built in the previous post. Remember we have a repository for the frontend app built with @vue/cli and another repository for the Laravel API. For this post, we are using the code from release v0.3 of the frontend app.
Outline of this post
- Combining the @vue/cli pwa plugin with workbox
- What is generated by @vue/cli when building for production?
- Precaching the app shell
- update previously cached items
- clean up items that are no longer needed
- cache new items
- Let the user decide when the updated service worker activates
- Start with automatically activating the updated SW
- Add button to let the user decide
- Runtime caching of fonts, css frameworks and so on
- Google font
- Font awesome via CDN
- Caching strategies
- Offline mode
- Does it work for the ‘about’ page?
- Lighthouse score
@vue/cli pwa plugin and workbox
We already have our vue.config.js in place and we don’t have to change a thing for this post! Our own service worker file is located here: /src/service-worker.js. When the app is built for production, the @vue/cli pwa plugin will inject a line into the service worker (located in /dist/service-worker.js) to import both the precache manifest and workbox.js (from CDN).
The precache manifest file is generated when the code is compiled and contains all assets (index.html, images, css and js files).
However, we have to connect the dots here to precache the app shell using workbox.
Precaching the app shell using Workbox
Precaching files with workbox is very simple and can be done with a single command:
The precache manifest (available in the service worker as self.__precacheManifest) is exactly in the right format (an array of strings) to pass to the precacheAndRoute method. Hence, we simply add this to the service worker file:
When you change your code and you build a new version of the app, a new service worker will be created automatically. When it is registered and installed, it will take care of cleaning up files that are no longer needed in the cache as well as caching new and updated files.
However, the new service worker will not be installed directly by default. On the contrary, the updated service worker is only activated once all active browser tabs where it is running are closed. As we strive for a seamless user experience, this is not really the desired behavior we are looking for. Ideally, we would like to show the user a ‘a new version is available’ message with a button to refresh the page.
Automatically activate the updated SW
We divide this UX feature in two parts. First we focus on automatic activation of the service worker without the message and button. This means that after a change in the code (and building the app for production), we can keep the browser tab open. Refreshing the app once will register the new service worker (however, the old cached files are still served) and quickly installs and activates it. However, it takes a second refresh to see your changes.
We can do better than this by adding a message with a button!
Let the user activate the updated service worker
We create this button in the main template in order to show it on all pages/ routes of the app. I won’t explain how we make this work in detail, because it has been written down already by Doug Allrich last december in his article on Medium.
In general it works like this:
- A custom event is dispatched from the registerServiceWorker.js when the service worker is updated.
- our central App component listens to this custom event and shows a refresh button when the event occurs.
- Once the user clicks the button, the active service worker sends a message to the new service worker (in waiting state) to activate it directly.
- Next, a ‘controllerchange’ event is fired (during activation of the new service worker)
- This event is caught and a page refresh is done automatically.
Note that the complete code is available on Github!
A real progressive web app also works when there is no internet connection. It offers an offline mode.
In the current setup, this is quite simple as nearly all assets we need to properly render the page are precached. Only the favicon images, stored in ‘/public/img/icons’ are missing. They are left out by default and we have to use the vue config file to overwrite this behavior.
Hence, we add the ‘exclude’ property to the vue.config.js, in the workboxOptions object:
This yields an offline mode for almost all situations.
However, if you navigate to the ‘about’ page, turn off internet and refresh, the ugly ‘there is no connection page’ appears.
To fix this, we have to register a navigation route. This makes sure that the index.html document is always served when a navigation occurs:
With this in place, the ‘/about’ route also works offline.
Caching strategies beyond precaching
Before we dive into this subject, let’s first add some Google fonts and the Font Awesome icons to our page. These are hosted online and we add links to the stylesheets in our /public/index.html file:
The Google fonts and icons won’t be precached as they are not part of our app shell, but external fonts and stylesheets. This means we have to use the runtime caching strategies that Workbox provides. I am not going into all the details for each caching strategy though: much has been written on this subject and the Workbox docs describe it pretty well.
Based on the common recipes that the Workbox docs provide, we’ll create two different routes to add the fonts to the cache.
All requests to ‘fonts.googleapis.com’ will use the StaleWhileRevalidate strategy (because the stylesheets may change frequently). On the other hand, requests to ‘fonts.gstatic.com’ are cached for one year using the CacheFirst strategy.
The requests to the bootstrapcdn for the Font Awesome icons are also cached with the StaleWhileRevalidate strategy. With this strategy, we are able to deliver a cached response very quickly while a network request is performed in the background to check for updates.
With the caching strategies in place, it is time to do a Lighthouse scan. This is Google’s tool to test webapps. The lighthouse score is shown below and the hard work clearly pays off. A score of 100 for PWA is great!
The first time I ran the audit, a score of 65 was returned for PWA. It told me to add a service worker… I was pulling my hair out because I have been working hard to get this service worker there in the first place.
Luckily, I was not alone and found this issue on Github. It seems that the Lighthouse tool has some problems when there is a lot of data to precache. I had some holiday pictures in the precache that weren’t minimized for use on the web. Since this is only a demo project I decided to remove these images and only show our company logo which is minimized. With this change I reran the Lighthouse scan which resulted in the beautiful score shown above.
Conclusion and remarks
In this post I have discussed how to setup a working PWA based on @vue/cli and Workbox. It’s generally quite easy once you know how to connect all bits and pieces: I only had to add a few lines of code to make the precaching work. And some extra lines for runtime caching. With this setup we have a PWA that works offline with a Lighthouse score of 100!
However, there are some topics left for future posts, for example:
- Handling the push notifications in offline mode (since this does not work when offline);
- Adding background sync;
- Adding Google Analytics to track users even if they are offline;
- And much more!