I recently had several days of extremely frustrating experiences with service workers. Here are a few things I've since learned which would have made my life much easier but which isn't particularly obvious from most of the blog posts and videos I've seen.
I'll add to this list over time – suggested additions welcome in the comments or via twitter.com/rich_harris.
Chrome 51 has some pretty wild behaviour related to console.log
in service workers. Canary doesn't, and it has a load of really good service worker related stuff in devtools.
^ no longer necessary
If you make a change to your service worker, then reloading the page won't kill the old one and activate the new one (it detects the change by requesting the service worker file each time and comparing the old and the new byte-for-byte), leaving you somewhat confused as to why your changes haven't taken effect. This is because the old window is never actually closed, meaning there's never a time to swap them out – you need to kill the tab completely then reopen it.
Or you can have the browser do that for you by going to the Application tab in devtools (in Canary, not stable Chrome yet), going to the Service Workers section, and checking 'Update on reload'.
For a while I thought that maybe the reason my changes weren't taking effect was because the old service worker was serving a cached version of itself, because it was intercepting all requests and caching them. So I was doing all sorts of daft stuff like registering service-worker.js?${Math.random()}
in an attempt to 'fix' it.
Turns out that when you call navigator.serviceWorker.register('service-worker.js)
the request for service-worker.js
isn't intercepted by any service worker's fetch
event handler.
The first time you load a page, navigator.serviceWorker.controller === null
. This continues to be true after a service worker has been successfully registered and installed.
(The relationship between navigator.serviceWorker.controller
and the service workers you can get via navigator.serviceWorker.getRegistration()
continues to be slightly confusing to me – particularly when it comes to knowing which service worker you're supposed to send messages to if you need to send messages. And don't get me started on self.skipWaiting()
and self.clients.claim()
, which on the face of it seem like a recipe for chaos. Though I'm sure it's just a matter of understanding when to use them.)
If you're using service workers you're probably caching the resources that make up your app shell. Perhaps, like me, you want to give the user to separately download content, e.g. fetch large files while they're on WiFi so they don't chew through their data plan. If so, you probably want some kind of progress notification.
My first instinct was to have the service worker do all the background caching (and checking of which files are already cached, importantly) and broadcast messages to connected clients. That sucks because service worker messaging sucks. But it turns out it's not necessary, because you can do it directly in the client:
document.querySelector( '.fetch-content' ).addEventListener( 'click', () => {
window.caches.open( myCache )
.then( cache => cache.addAll( content ) )
.then( () => alert( 'content is now available offline' ) )
.catch( () => alert( 'oh noes! something went wrong' ) );
});
(Obviously you'd probably want a more granular strategy that made it possible to report download progress, but you get the idea.)
A lot of people have experienced the same frustrations you have and know how to fix them. In particular, Jake and other folks at Google and Mozilla involved in implementing this stuff are unreasonably helpful if you reach out to them (not that I encourage you to spam their mentions every time you get stuck, but if you really need help...).
I still think the API is a bit lumbering in several places (if the JS convention for the naming relationship between instances and classes is foo = new Foo()
, why is navigator.serviceWorker
an instance of ServiceWorkerContainer
while navigator.serviceWorker.controller
is an instance of ServiceWorker
? And what the hell is a ServiceWorkerRegistration
? Never mind all the many other interfaces we now need to learn), and I'm worried about how learnable this stuff is by people who don't have the time and inclination to study hard, but at the very least knowing the stuff above has made my experience with service workers much nicer.
- If I have code in my service worker that runs outside an event handler, when does it run?
- Probably some other things I've forgotten for now
Upvote on this comment!
This is very understated where information can be found and super important. If you want to serve your service worker in a sub-root directory, but allow it to claim clients above that scope, you'll need to do this ^ or add a
Service-Worker-Allowed: /
header with the server response headers that serves the sw.js script.For example, I was serving my Google workbox service worker from
/assets/js/sw/sw.js
, and it installed just fine. I couldn't for the life of me figure out how/why it was installing, but not intercepting any network requests. This is because the default scope was/assets/js/sw
meaning only assets with that path and lower would be intercepted.The solution is to serve the sw.js asset with a
Service-Worker-Allowed: /
and to register the script with that scope:This will allow you to serve your script from a path other than root, but will allow the higher scope, but only with the response header:
P.S. I might have stumbled upon this important scope information on Google's introduction to Service Workers - Registration and Scope section. Man, it's hard to learn things with a habit of skim reading. :)