Letting PWA users know there's new code ready

Progressive Web Apps are, if nothing else, an exercise in cache management. Do it right and everybody is happy. Do it wrong and the ghost of app past may haunt a user until they buy a new phone.

The first thing you need to do is prevent your browser cache from engaging in hand-to-hand combat with your service worker cache. Specifically, do not cache service-worker.js. That sucker needs to pull a Mission Impossible and self immolate after opening. In Nginx, your caching strategy would look something like this:

1
2
3
4
5
6
# cache settings
location ~ (service-worker.js|sw.js|index.html)$ {
expires off;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
}
...other sensible caching policies etc.

If your service worker is cached in the browser, you end up having to do weird stuff to refresh, like close the actual browser tab and open it again. When you CTRL-F5 with the service worker cached, the browser basically laughs at you until the tab itself goes away. It knows there’s new content. But it hates you.

Once you have that straightened out, you’re still stuck with the issue of a service worker loading new content but not actually doing anything with it until the user refreshes the page. This isn’t terrible, unless the new code fixes a bug, in which case it is in fact terrible.

I took a page from the Vue CLI docs here. They give a notice when there’s new content available with a button that essentially does a location.reload(true). You can roll that by hand, but if you’re using a convenience library like register-service-worker, it’s a bit easier.

Your default register-service-worker code looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { register } from 'register-service-worker'

register('service-worker.js', {
registrationOptions: { scope: './' },
ready(registration) {
console.log('Service worker is active.')
},
registered(registration) {
console.log('Service worker has been registered.')
},
cached(registration) {
console.log('Content has been cached for offline use.')
},
updatefound(registration) {
console.log('New content is downloading.')
},
updated(registration) {
console.log('New content is available; please refresh.')
// good place to send the user a message!
},
offline() {
console.log('No internet connection found. App is running in offline mode.')
},
error(error) {
console.error('Error during service worker registration:', error)
}
})

Aside from very convenient log messages, this gives you a nice updated hook you can use to send the user a message. In index.html I have a little message on the bottom of the screen with a hidden class so it doesn’t display by default.

1
2
3
4
5
6
7
8
9
<!-- service worker detected update -->
<div class="alert-footer fixed bottom-0 right-0 mr-3 mb-3 hidden" id="swUpdate">
<div class="flex items-center justify-between w-full p-2 shadow-lg rounded bg-gray-200">
GeoPortal has been updated.
<button class="btn bg-blue-800 text-white shadow hover:bg-blue-900 hover:shadow-md ml-3 capitalize" onclick="location.reload(true)">reload</button>
<button class="btn shadow-none hover:bg-gray-400 hover:shadow ml-1 text-gray-700"
onclick='document.querySelector("#swUpdate").classList.add("hidden")'>dismiss</button>
</div>
</div>

This has two buttons, one to dismiss by slapping a hidden class back on the parent, and one to reload the page. It looks like this:

imgur

In my service worker registration I add this line to the updated hook to show the update notification:

1
2
3
4
updated(registration) {
console.log('New content is available; please refresh.')
document.querySelector("#swUpdate").classList.remove("hidden")
},

This message is only shown when the service worker is done loading the update, and clicking reload gets the user up to snuff. If a user doesn’t want to reload then, they don’t have to. While it would be easier to stick location.reload(true) directly in the service worker updated hook and be done with it, since it may take a few moments after page load for the service worker to register and download updates, you’ll essentially yank the rug out from under your users mid page-oggle. I’m sure there’s a proper UX phrase for that but I’d just call it bad manners.