Home / Notes

Offline mode with service workers

2nd April 2026

Create a new JS file called service-worker.js. Define a cache key and add the installation function. Pass it all the static assets you wish to cache.

const CACHE_NAME = "v1";

self.addEventListener("install", (event) => {
  event.waitUntil(
    (async () => {
      const cache = await caches.open(CACHE_NAME);
      await cache.addAll([
        "/", "/index.html", "/index.js", "/icon.svg", "/manifest.json"
      ]);
    })(),
  );
});

Now let's throw in the function that runs on activate. This will clear out all the old caches that do not match the current CACHE_NAME value.

One important thing to know is that the activate event will only fire when all other pages using the old service worker have closed.

self.addEventListener("activate", (event) => {
  event.waitUntil(
    (async () => {
      const cachesKeys = await caches.keys();
      cachesKeys.map((key) => key !== CACHE_NAME && caches.delete(key));
    })(),
  );
});

Now for the main event. This respondWith function on fetch will intercept all requests and carry out anything we want. There are many cacheing policies you can do here. For this one, we're going to check for a cache hit and respond with that straight away whilst fetching and cacheing a new response in the background. If there is no cache hit, we'll fetch and cache the response as normal.

self.addEventListener("fetch", async (event) => {
  event.respondWith(
    (async () => {
      const cache = await caches.open(CACHE_NAME);
      const cachedResponse = await caches.match(event.request); // [1]
      if (cachedResponse) {
        asyncFetchAndUpdateCache(event.request, cache); // [2]
        return cachedResponse;
      }
      const response = await fetch(event.request);
      if (response.ok) await cache.put(event.request, response.clone());
      return response;
    })(),
  );
  async function asyncFetchAndUpdateCache(request, cache) {
    const newResponse = await fetch(request);
    if (newResponse.ok) await cache.put(request, newResponse.clone());
  }
});
  1. We are checking caches rather than our own current cache because we want to check everything available.
  2. This is an async function and we're calling it without await so the program will carry on and return the cachedResponse whilst it fetches in the background.

The last thing we have to do is register the service workers in your main script.

navigator.serviceWorker?.register("/service-worker.js");

Note: If you are running TypeScript, you are going to want to add WebWorker to your compilerOptions.lib in the TS config, and add declare let self: ServiceWorkerGlobalScope; to your service worker.