Random notes around service workers development and testing
Hey!
Here are a few random tips and tricks I learned through the years around service workers development and testing.
Helpful service worker resources
Reloading a service worker
Reloading a page won’t update/remove the previous version of its service worker.
If you’re using Chrome, to ensure you’re using the latest version of your service worker, tick the “Update on reload” toggle in the “Application” ⭢ “Service Workers” section of the Chrome DevTools.
Simulate a network condition
To simulate a network condition (e.g., offline, 3g, etc…) in a service worker in Chrome, untick the “Update on reload” toggle.
I’m still not sure why/if this is really needed. In a few cases, when the toggle was enabled and I was simulating an offline mode, the service worker was still going through the network ?♂️
Service workers don’t intercept other service worker registration events
Service workers can’t intercept network requests made by the navigator.serviceWorker.register("sw.js")
API. This means you don’t have to worry about service workers caching themselves.
Testing exceeded quota errors on service workers
Each browser has a different limit on the cache storage available for a service worker. On desktop browsers, it will be based on the percentage of available space on disk, so testing (in development) what happens when the cache quota exceeds can be tricky.
A possible workaround is:
- Use Chrome in incognito mode, which has a hard limit of 100MB.
- Manually cache huge assets (e.g., 8k resolution images).
- Watch the quota exceed.
Service workers don’t run in Firefox private mode
In Firefox, Service Worker APIs are hidden and cannot be used when the user is in private browsing mode.
Service workers registration fails on Firefox if cookies are disabled or set to be cleared on quit
Registering a Service Worker in Firefox throws a “The operation is insecure.” exception if cookies are disabled or set to be cleared on quit.
Service workers and Chrome incognito mode
You may find it helpful to test your service worker in an Incognito window so you can close and reopen it, knowing that the previous service worker won’t affect the new window. Any registrations and caches created within an incognito window will be cleared once that window is closed.
That said, please keep in mind that single incognito tabs and windows are not sandboxed. So, if you have two different incognito windows open simultaneously, they’ll share cookies, storage data, and service workers.
Force-reload
If you force-reload the page (shift-reload), it bypasses the service worker entirely. It’ll be uncontrolled. This feature is in the spec, so it should work in any browser that supports service workers.
Serving a service worker on immutable paths
It’s against best practices to use an immutable path for service workers (e.g., if the service worker has a hash in its name). However, if you’re not using the service worker to cache the HTML that serves it, you shouldn’t worry about it.
How often is a service worker updated?
A service worker will update every 24 hours or when all pages/instances are closed.
Keep a no-op service worker handy
If you don’t want to leave your users running a buggy service worker code while you take the time to work out a solution, it’s a good idea to keep a simple, no-op service-worker.js handy, like the following:
self.addEventListener("install", () => { self.skipWaiting();
}); self.addEventListener("activate", () => { self.clients.matchAll({ type: "window" }).then((windowClients) => { windowClients.forEach((windowClient) => { windowClient.navigate(windowClient.url); }); });
});
Related post on StackOverflow here.
Get the “client” sender instance from a postMessage
In a service worker message listener, event.source
is the client instance of the sender.
self.addEventListener("message", (event) => { const senderClient = event.source; senderClient.postMessage("?");
});
Clean old cache
It’s a good idea to version the service worker cache by hardcoding a version number in the service worker. You can then bump it whenever you want to clear the old cache (e.g., because you added a new caching logic).
Here’s an example of how you can clean your cache (using Workbox):
import { cacheNames } from 'workbox-core';
import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies'; const SERVICE_WORKER_VERSION = "v1"; const caches = [ {name: 'images', matcher: new RegExp('/static/images')}, {name: 'fonts', matcher: new RegExp('/static/fonts')}
]; caches.forEach({name, matcher} => { registerRoute( matcher, new CacheFirst({ cacheName: `${name}-${SERVICE_WORKER_VERSION}` }) )
}); self.addEventListener('activate', (event) => { async function cleanupOldCaches() { const cachesToKeep = caches.map(({ name }) => `${name}-${SERVICE_WORKER_VERSION}`); cachesToKeep.push(cacheNames.precache, cacheNames.runtime, cacheNames.googleAnalytics); const allCaches = await caches.keys(); const cachesToCleanup = allCaches.filter((cache) => !cachesToKeep.includes(cache)); for (const cacheToCleanup of cachesToCleanup) { await caches.delete(cacheToCleanup); const cacheExpiration = new CacheExpiration(cacheToCleanup, { maxEntries: 1 }); cacheExpiration.delete(); } } event.waitUntil(cleanupOldCaches());
});
Related discussion in the Workbox repo here.