Skip to content

Instantly share code, notes, and snippets.

@Rich-Harris
Last active March 28, 2024 15:08
Show Gist options
  • Save Rich-Harris/fd6c3c73e6e707e312d7c5d7d0f3b2f9 to your computer and use it in GitHub Desktop.
Save Rich-Harris/fd6c3c73e6e707e312d7c5d7d0f3b2f9 to your computer and use it in GitHub Desktop.
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
@nivle
Copy link

nivle commented Jun 18, 2021

self.addEventListener("install", event => {
    self.skipWaiting();
});

Adding self.skipWaiting(); fixes the service not updating issue, though the service worker may, or may not be doing something important...
https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting

There is also update, but that wont update the service worker if its active or something like that(so not very reliable)...
https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/update

@FreedomLiberty1776
Copy link

For your first point, you can use self.skipWaiting() after installing. This will kill the old service worker and activate the new one. You don't have to close the page and reload again.

@ajlozier
Copy link

Have you ever encountered an issue where after posting SKIP_WAITING and executing a page refresh, the page just spins endlessly? A second page refresh works as expected. This happens only intermittently for me. I have seen it mentioned a few times on various Github issue threads, etc., but have never found a clear answer as to what might cause this.

@frank-dspeed
Copy link

frank-dspeed commented Dec 5, 2021

@ajlozier your missing the on activation event when you install a new service worker the old one does not get activated from alone.

you should use a framework like workbox from google as this abstracts such complex scenarios service workers are not a easy term you need to learn a lot about caching and server management.

  • install new service worker wait till it is installed (event)
  • wait and unload the existing service worker
  • wait again and activate the new one

all this needs a lot of code that includes a lot of failure handling.

@guest271314
Copy link

This stackoverflow question Cache Service worker for localhost can't be remove and unregistered describes the issue where when URL is localhost and unregister() is called the ServiceWorker is not unregistered.

This appears to be a Chromium/Chrome bug. The behaviour is not reproduced on Firefox 95. (Chromium authors banned me bugs.chromium else I would file this there).

The workaround I am currently using is to register the ServiceWorker with a query component. Then the ServiceWorker will be unregistered.

for (const registration of await navigator.serviceWorker.getRegistrations()) {
  try {    
    await registration.unregister();    
  } catch (e) {
    console.error(e);
  }
}
// without including query component the above will not unregister the ServiceWorker
const sw = await navigator.serviceWorker.register(`sw.js?${new Date().getTime()}`, {
  scope: './',
});

@Playit3110
Copy link

Playit3110 commented Dec 23, 2022

Hello all, I have now consumed so much SW stuff that i cant think right anymore.

I build a proxy service and try to change headers, but it doesn't work. Or I send a header but the service worker just deletes it (in my case the Set-Cookie header), maybe because the origin doesn't match the request origin, because i changed them in the request before i forwarded it to a proxy server.

@guest271314
Copy link

@Playit3110 You should be able to intercept the request in onfetch and set headers with respondWith(). You can alternatively use a Chrome extension with declarativeNetRequest to modify or remove headers, see https://github.com/guest271314/remove-csp-header.

@pspraveenkr
Copy link

I wish I read this article few days ago when I started on a service worker project. I was able to piece together the architecture and relationships between various instances and properties but could've gone a lot easier if I had access to notes from folks who walked this path before.

@m0ntana
Copy link

m0ntana commented Dec 22, 2023

You spent over an hour?
I spent around 4 hours trying to find this out. Thank you, this saved my day.

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