Skip to content

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.

^ no longer necessary

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.

Copy link

commented Jul 21, 2016

Thanks! 👍

@royriojas

This comment has been minimized.

Copy link

commented Jul 21, 2016

👍

@ramiabraham

This comment has been minimized.

Copy link

commented Aug 4, 2016

Has already been useful, thanks ❤️

@julienw

This comment has been minimized.

Copy link

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.

Copy link

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.

Copy link

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.

Copy link

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.

Copy link

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.

Copy link

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.

Copy link
Owner Author

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.

Copy link

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.

Copy link

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.

Copy link

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.

Copy link

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.

Copy link

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.

Copy link

commented Nov 1, 2017

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

@ashinzekene

This comment has been minimized.

Copy link

commented Nov 11, 2017

You are a blessing

@mike092235

This comment has been minimized.

Copy link

commented Nov 11, 2017

Thanks 100x for this!

@mesqueeb

This comment has been minimized.

Copy link

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.

Copy link

commented Dec 23, 2017

Really helpful

@axchanda

This comment has been minimized.

Copy link

commented Jan 16, 2018

You saved my day, buddy!

@DavidAPowers

This comment has been minimized.

Copy link

commented Jan 30, 2018

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

@rjsteinert

This comment has been minimized.

Copy link

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.

Copy link

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.

Copy link

commented Mar 16, 2018

👍

@matthewmueller

This comment has been minimized.

Copy link

commented Mar 23, 2018

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

@lbssousa

This comment has been minimized.

Copy link

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.

Copy link

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.

Copy link

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.

Copy link

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.

Copy link

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.

Copy link

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.

Copy link

commented Aug 23, 2018

😍

@dsmithhfx

This comment has been minimized.

Copy link

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.

Copy link

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.

Copy link

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.

Copy link

commented Nov 13, 2018

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

@skippy123

This comment has been minimized.

Copy link

commented Jan 3, 2019

That was super helpful , thanks 👍

@chetanraj

This comment has been minimized.

Copy link

commented Jan 3, 2019

@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?

@mesqueeb - were you able to solve this..

@ndom91

This comment has been minimized.

Copy link

commented Jan 4, 2019

Chrome DevTools now (for a while?) has the option to "Update on Reload" the sw which seems to work pretty well.

image

@jirihradil

This comment has been minimized.

Copy link

commented Jan 10, 2019

Thanks! 👍

@chab

This comment has been minimized.

Copy link

commented Jan 28, 2019

Definitely a page I wish I'd known sooner!

@makmm

This comment has been minimized.

Copy link

commented Apr 2, 2019

i have filenames with hashes that are injected using serviceworker-webpack-plugin to the service worker. how do i reload the service worker so it reloads the file list?

@AlexBorsody

This comment has been minimized.

Copy link

commented May 3, 2019

Same question here @makmm, I am just hardcoding the URLs now, also I am using Drupal which aggregates the css/jss so the file name would not be parsed by one of the build tools that inject and manifest. Also, do you need to rebuild every time the assets change?

@alexburlton

This comment has been minimized.

Copy link

commented May 14, 2019

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!

@chrisblakley - Thank you! Think you've just saved me spending way too long on the same thing!

@parties

This comment has been minimized.

Copy link

commented May 15, 2019

Another good resource: https://codelabs.developers.google.com/dev-pwa-training/

A number of the pitfalls mentioned here appear to be brought up (though not always as obviously as here).

@augustofagioli

This comment has been minimized.

Copy link

commented Jun 11, 2019

I believe the "Canary/Stable" thing needs a review, here

@garygreen

This comment has been minimized.

Copy link

commented Jun 14, 2019

Total noob question but can't really find answer to this anywhere.

Does cache.addAll(['/some-url']) add all the resources for that url to the cache? E.g if that url returns with css, js, images, etc do they all get added to the cache? From testing it seems they do, which I find a bit strange because what if one of the files is a massive video or something?

I personally would just expect the HTML for that page to be cached, not the individual resources unless you've specifically told it to? Or maybe I'm just testing this wrong.

@Rich-Harris

This comment has been minimized.

Copy link
Owner Author

commented Jun 15, 2019

@augustofagioli good catch, thanks. updated

@garygreen 99% sure that it will only fetch that specific resource. there must be something else going on with your setup

@NishantDesai1306

This comment has been minimized.

Copy link

commented Jun 16, 2019

I am using workbox to create my service worker but the problem is that service worker caches every document request and I am using failing of request that expect text/html content to determine that my PWA is offline.
But as all document type requests are getting cached and then are served by service worker so when I simulate offline mode my app still gets html page from cache and my PWA does not show offline.html.

Any help is appreciated.

@ebbesand123

This comment has been minimized.

Copy link

commented Jul 2, 2019

@NishantDesai1306
One way to do it is to simply fetch the request in your fetch handler and return it in the catch block.

// Example of returning offline.html when fetching fails
self.addEventListener('fetch', event => {
    event.respondWith(
        fetch(event.request).catch(() => {
            return new Response("<h1>Ooopsie! You seem to be offline!</h1>", {
                headers: {'Content-Type': 'text/html'}
            });
        })
    )
});
@NishantDesai1306

This comment has been minimized.

Copy link

commented Aug 5, 2019

I am also facing some other issue now, when I update my service worker the browser installs it and keeps it in wait state which is expected but my issue happens when I call self.skipWaiting(), it should move my service worker from waiting state to active state but that is not happening more over when I try to manually activate it by pressing skipWaiting() in dev console and even then it is not getting activated unless I manually stop currently running server worker.

I am new to service worker always first consider that I have messed up something even if it is some basic concept related to service worker.

For more info I have also created a post on stackoverflow: https://stackoverflow.com/questions/57348446/skipwaiting-is-not-installing-new-service-worker-which-is-stuck-in-waiting-pha

here's a gif of what's happening
image

@codingchili

This comment has been minimized.

Copy link

commented Aug 8, 2019

I'm trying to implement the network-then-cache with cache updates.

For some reason I was seeing hour old content loaded in offline, even through I've updated the cache name/version. This is with service worker updates enabled in devtools -> application.

Now I'm looping over all the caches in window.cache and deleting them. Finally it works, no idea why. There seems to be some kind of difference between enabling offline and just killing the http server as well.. hmm.

@intrepidOlivia

This comment has been minimized.

Copy link

commented Sep 13, 2019

Does cache.addAll(['/some-url']) add all the resources for that url to the cache? E.g if that url returns with css, js, images, etc do they all get added to the cache?

@garygreen Did you ever figure this out? Because this is the result I want to achieve (store all images, css, etc. referenced at a single URL). You wanted to avoid this happening. But how do I make it happen?

@garygreen

This comment has been minimized.

Copy link

commented Sep 16, 2019

@intrepidOlivia From testing I believe it simply caches the source/html/response from that one page and not anything that it links to. Technically though, anything that the page links to can be cached as well because they will all fire fetch events in the service worker, so you can cache all the page contents and stuff as long as you cache them in the fetch event.

@remy90

This comment has been minimized.

Copy link

commented Sep 26, 2019

Anyone know how up to date this doc is? I can see some revisions..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.