Skip to content

Instantly share code, notes, and snippets.

@Rich-Harris
Last active December 18, 2024 21:13
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
@intrepidOlivia
Copy link

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
Copy link

@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
Copy link

remy90 commented Sep 26, 2019

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

@benstov
Copy link

benstov commented Dec 19, 2019

for those who use node & express and wish to create a PWA app.
highly recommend using offline-webpack-plugin here, it makes it all workout out of the box, maybe a bit of configuration but it really does it all, it generates the sw.js with timestamp and caches everything (except for routes that return index.html - for example in react apps, in that case you need to add those routes to externals option inside the OfflinePlugin)
*** don't forget to unregister existing service workers - recommending reading this: https://ddcode.net/2019/05/30/use-offline-plugin-with-webpack-to-easily-implement-pwa/

For localhost usage and tests
If you wanna check the service-worker in localhost you have to run the page under https, for that i recommend doing it by using devcert, it will generate a certificate that you can just pass to https.listen
example:

    const app = express();
    const https = require('https');
    const ssl = await require('devcert').certificateFor('localhost');

   // .....
   // all of the app.use functionality
   // .....

    const httpsServer = https.createServer(ssl, app);
         httpsServer.listen(8080, () => { 
             console.log('App is running on https://localhost:8080');
     });

@0xMelkor
Copy link

Hi Guys,

are service workers able to intercept XHR/Fetch requests of my application, or are they just responsible for static assets requests (.html, .js, .png ecc)?

This is an open point for me.

Thank you very much,

@frank-dspeed
Copy link

frank-dspeed commented Mar 20, 2020

@insanediv it intercepts every request done by the code on the page that the service worker controls no matter what API u use even the browser internal once.

see it as the universal request handler inside your whole Application

@rouftom
Copy link

rouftom commented Apr 1, 2020

Thanks, you saved my life.

@stepgrigoryan
Copy link

@Rich-Harris,
first of all thank you for sharing all this stuff and saving our time and mental health.
At this moment I am still struggling with maybe a simple issue, but can't figure out how to implement it, although I've read a ton of articles, questions,etc about service workers.
All I want is to simply update my service worker immediately in background whenever I make a change in my service-worker.js file, without all this complication and spaceship building knowledge.
Is this possible?
Thank you in advance.

@frank-dspeed
Copy link

@GeorgeTarzi Maybe consider using a framework then you don't need to dig into the deep details. https://developers.google.com/web/tools/workbox

@one909
Copy link

one909 commented May 27, 2020

ขอบคุณครับ🤨

@Lucent
Copy link

Lucent commented Aug 22, 2020

Thanks for this, specifically about caches being available from window. I nearly lost my mind trying to postMessage to tell the SW to add specific URLs to the cache on install, but serviceWorker.controller(.postMessage) is null until activation and I can't use waitUntil on the install event because the current page isn't in the URLs SW initially caches (one SW controlling many translations).

@jdillick
Copy link

jdillick commented Oct 28, 2020

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.

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:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/assets/js/sw/sw.js', { scope: '/'});
}

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

@rlau89
Copy link

rlau89 commented Dec 2, 2020

I have a Java web app which runs 3 projects, 2 of which are served to the client. They are served from their relative project folders and code served from their paths i.e.
host/Accounts/ host/Builder/
I am currently attaching the service from host/Accounts/ as the source is in that project but need to attach it host/ however I am not sure if this is possible?

@douglasg14b
Copy link

If you have to use the dev tools to update on reload, or do a no-cache reload. How are users expected to get an update for the PWA they are on if you can't do so programmatically?

Service worker exists under waiting field with a state of installed. But there is no clear way to move forward with that? Reloading the page leaves it in the same state...

@akoidan
Copy link

akoidan commented Feb 6, 2021

Does anyone now why service worker sloooows down loading content of the page? I'm using SPA and I added all static assets to service worker. Considering the fact that accessing static resources from cache should be faster than from network I expect my site to load in miliseconds, not within 6 seconds range. Before SW loading speed was about 2-3s, now it's 5-7. Here's my SW code

Is it supposed to be so, or I am doing something wrong?

@akoidan
Copy link

akoidan commented Feb 9, 2021

Yes it helped. @ch1c0t any idea why passing request via fetch from service worker and cache takes that long?

@ch1c0t
Copy link

ch1c0t commented Feb 10, 2021

@akoidan Glad to know. Precaching in the install event can degrade the loading speed initially.

Regarding the fetch event, sometimes it might be advantageous to return as early as possible. For your use case -- can you think about more cases when it could make sense to return before calling event.respondWith?

@pixiekat
Copy link

pixiekat commented Jun 1, 2021

This helped me far more than the official docs by Google and Mozilla.

@danny-wang
Copy link

to

@chetanraj do u find a way to solve this problem?

@danny-wang
Copy link

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

@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