Instantly share code, notes, and snippets.

Embed
What would you like to do?
Stuff I wish I'd known sooner about service workers

Stuff I wish I'd known sooner about service workers

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.

Use Canary for development instead of Chrome stable

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.

Reloading the page doesn't behave as you'd expect

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'.

The new service worker isn't fetched by the old one

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.

navigator.serviceWorker.controller is the service worker that intercepts fetch requests

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.)

You can access self.caches in the browser as window.caches

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.)

You're not alone

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.

Stuff I still don't understand

  • 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
@jacobmischka

This comment has been minimized.

jacobmischka commented Jul 21, 2016

Thanks! 👍

@royriojas

This comment has been minimized.

royriojas commented Jul 21, 2016

👍

@ramiabraham

This comment has been minimized.

ramiabraham commented Aug 4, 2016

Has already been useful, thanks ❤️

@julienw

This comment has been minimized.

julienw commented Aug 8, 2016

going to the Application tab in devtools (in Canary, not stable Chrome yet),

In stable, I think it's in a "Resources" section.

checking 'Update on reload'.

This didn't really worked for me, I still have to click "skip waiting" manually :(

If I have code in my service worker that runs outside an event handler, when does it run?

As I understood, it's not guaranteed to run at all.

Did you manage to make service workers work when developing locally without a proper TLS cert ? Or even without https ?

@bransbury

This comment has been minimized.

bransbury commented Aug 8, 2016

Chrome 52 is rolling out now and seems to have these updates - so should be able to use Chrome stable instead of Canary once you have the update 😀

@jeffposnick

This comment has been minimized.

jeffposnick commented Aug 8, 2016

Just a quick shout-out that the service-worker tag on Stack Overflow gets a lot of attention from browser DevRel teams, and we're always happy to help there.

@jeffposnick

This comment has been minimized.

jeffposnick commented Aug 8, 2016

@Rich-Harris, I took the liberty of moving your "If I have code in my service worker that runs outside an event handler, when does it run?" question to Stack Overflow, and answered it there, since I know a lot of other folks have found this confusing as well.

@marcoscaceres

This comment has been minimized.

marcoscaceres commented Aug 9, 2016

Talked about these problems recently - as I also hit all of them, and got burnt by cache invalidation:
https://marcoscaceres.github.io/codeconf-talk/#/

Video coming soon.

@simevidas

This comment has been minimized.

simevidas commented Aug 9, 2016

Nobody said that shipping a low-level API is going to be easy ^_^; Service worker libraries to the rescue.

@Rich-Harris

This comment has been minimized.

Owner

Rich-Harris commented Aug 10, 2016

@julienw:

Did you manage to make service workers work when developing locally without a proper TLS cert ? Or even without https ?

Works fine on http://localhost:xxxx. If I need to test on a device I use ngrok

@jeffposnick awesome!

@dandv

This comment has been minimized.

dandv commented Aug 12, 2016

Thanks for documenting these lessons!

BTW, GitHub doesn't notify people of comments on gists (someone even made a petition urging them to fix this 3-year old issue); might be worth putting this into a regular GitHub repo if more discussion is desired.

@chrisblakley

This comment has been minimized.

chrisblakley commented Jun 26, 2017

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.

I'm fighting with this now. When does it become not null (it's always null for me even after a reload)? What if I want to postMessage() on the first page load?

@juanmirod

This comment has been minimized.

juanmirod commented Aug 17, 2017

Nice summary,to those still struggling: you can find a great introduction to Service Workers in udacity (https://www.udacity.com/course/offline-web-applications--ud899) Jake Archibal is one of the teachers and they give you a lot of development tips and you build a sample project from scratch along the course (Here is mine, with more links about SW in the Readme https://github.com/juanmirod/trains)

@tozz

This comment has been minimized.

tozz commented Sep 5, 2017

@chrisblakley Did you ever manage to solve this? I have the same need and the API seems to be designed with hostility in mind, why isn't there a working event for when the installed (and even better, updated) Service Worker is actually available to accept messages, blows my mind.

@valioDOTch

This comment has been minimized.

valioDOTch commented Oct 26, 2017

Actually using "service-worker.js?${Math.random()}" can still be a good idea, in case you're splitting up your service worker across multiple files.

The "service-worker.js" in my project is basically a list of importScript statements.
The problem that can arise from this, is that when changing any of those imported scripts e.g. Chrome won't realize that they were changed, because it only downloads and makes a comparison of "service-worker.js, but not the imported scripts.

Thus I always use such a "cache-bust parameter" for the registration and all importScript statements.

@vogelbeere

This comment has been minimized.

vogelbeere commented Nov 1, 2017

Thanks so much for this - downloaded Canary and cleared the application cache, problem solved!

@ashinzekene

This comment has been minimized.

ashinzekene commented Nov 11, 2017

You are a blessing

@mike092235

This comment has been minimized.

mike092235 commented Nov 11, 2017

Thanks 100x for this!

@mesqueeb

This comment has been minimized.

mesqueeb commented Dec 3, 2017

@Rich-Harris,
Every time I update something in my code and I deploy. When a user opens my site again he'll only fetch the new service worker, but still use the old cached files. This is until he refreshes a second time...

Therefore, every time I deploy something new, ask a friend to check it out, he won't see it the first time he tries to open it...
Any ideas how to prevent this?

@daniele-pecora

This comment has been minimized.

daniele-pecora commented Dec 23, 2017

Really helpful

@axchanda

This comment has been minimized.

axchanda commented Jan 16, 2018

You saved my day, buddy!

@DavidAPowers

This comment has been minimized.

DavidAPowers commented Jan 30, 2018

@tozz, @chrisblakley
I'm struggling with the same issue, any luck on this?

@rjsteinert

This comment has been minimized.

rjsteinert commented Feb 12, 2018

a more granular strategy that made it possible to report download progress

Great post! Any ideas on how to get info on progress? I've resorted to looking for APIs that explain how much has been stored in Service Worker cache as well. If we can check that, we can get an idea of progress.

@chrisblakley

This comment has been minimized.

chrisblakley commented Feb 18, 2018

One thing that took me way too long to realize is that shift + reload will bypass the service worker and the window will not be controlled!

@eqbal

This comment has been minimized.

eqbal commented Mar 16, 2018

👍

@matthewmueller

This comment has been minimized.

matthewmueller commented Mar 23, 2018

Thanks @Rich-Harris – this saved me a bunch of time ❤️

@lbssousa

This comment has been minimized.

lbssousa commented Apr 5, 2018

One more thing to know: if your PWA has local SWF files, it's pointless to precache them for offline usage, since your service worker, by design, WON'T respond to requests from plugins (e.g. via <embed> or <object> tags). References:

https://www.chromestatus.com/feature/6313531834105856

https://w3c.github.io/ServiceWorker/#implementer-concerns

@fijiwebdesign

This comment has been minimized.

fijiwebdesign commented Apr 29, 2018

I spent over an hour trying to figure out why the fetch event wasn't firing. The service worker must be in the root directory / for fetch to fire on that root "scope". I had /js/sw.js as the service worker and had to move to /sw.js.

@natanio

This comment has been minimized.

natanio commented May 8, 2018

Any idea what might cause navigator.serviceWorker.register() to get skipped completely? This happened in Firefox until it randomly didn't. Now it happens in Safari. Thanks!

I have the following:

if ('serviceWorker' in navigator) {
    // My code makes it here.
    navigator.serviceWorker.register('/sw.js').then(registration => {
	
        //... But I never get in here

    }).catch(err => console.log('Service Worker Error', err)); // <- never see this
} else {
    console.log('serviceWorker not supported'); // <- nor do I see this.
}
@anuraggautam77

This comment has been minimized.

anuraggautam77 commented May 29, 2018

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
I am struggling with same, every time I am getting null. because of that I am not able to use postmessage() method.

@strongSoda

This comment has been minimized.

strongSoda commented May 30, 2018

Actually in my case when I deploy a new build and revisit the site it loads with the new build but not the home screen added standalone app. For that I have to clear the site cache and then the home screen added app fetches the changes. Not sure how to get that working too.

@Malvoz

This comment has been minimized.

@Aeners

This comment has been minimized.

Aeners commented Jul 11, 2018

Thanks a lot for this !
Unfortunately not yet on the front-page of the service workers documentation.

@fatihacet

This comment has been minimized.

fatihacet commented Aug 23, 2018

😍

@dsmithhfx

This comment has been minimized.

dsmithhfx commented Sep 27, 2018

So this rant is two years old already, and I just 'discovered' service workers cache API, and a lot of what you said (two years ago!) is ringing true for me, today. After an initial, taste of the plain vanilla sw cache API, which was sort of positive, I decided to go through "sw-toolbox", the cache library (which has since been deprecated for something called "Workbox" -- nuclear overkill for my use case).

Still true: poor documentation (assumes you were the original developer, and can fill in all the blanks).

Still true: really difficult cache update/refresh/reload stuff.

Still true: sw cache ("cache storage") conflicts with, and fails to take precedence over native browser cache and memory (IME).

Until I figured out that last one by trial-and-error, I spent three hours trying to understand why "networkFirst" could not load a fresh site css file from the network, instead of a days-old version from memory/cache/mars /jupiter.

Phew!

@pertoyra

This comment has been minimized.

pertoyra commented Oct 19, 2018

@tozz, @chrisblakley, @DavidAPowers, @anuraggautam77 You have probably solved the problem of a null controller by now since its been a while. :) But I thought I add a solution here as for future reference.

As described by Jake Archibald here:
w3c/ServiceWorker#799 (comment)

This can be accomplished by something like:

const promise = new Promise(resolve => {
  if (navigator.serviceWorker.controller) return resolve();
  navigator.serviceWorker.addEventListener('controllerchange', e => resolve());
});

promise.then(() => {
  navigator.serviceWorker.controller.postMessage(...);
});
@dhilst

This comment has been minimized.

dhilst commented Oct 28, 2018

Thanks! 👍
I spend about 2 hours until I kill the tab to understand why my service worker was not updating. So I googgled and get here as confirmation. Totally confusing!

@sparkison

This comment has been minimized.

sparkison commented Nov 13, 2018

thanks for this! Straight to the point, very helpful when getting started with service workers and building PWAs 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment