Using Service Workers with Remix

A Remix app is by default fast, the framework optimize a lot how the required resources for a page are loaded so we don't have waterfalls, we can easily add more resources like images or fonts so they are loaded faster too.

But if you want to build an installable PWA or make your app work offline or just optimize as much as possible the load of your Remix application a Service Worker is powerful tool to use.

Creating a Service Workers

First we will create a sw.js file inside the public folder, this means the URL of the Service Worker will be /sw.js, super short and simple.

Inside this file we can add anything but here's a simple example.

self.addEventListener("fetch", (event) => {
  let url = new URL(event.request.url);
  let method = event.request.method;

  // any non GET request is ignored
  if (method.toLowerCase() !== "get") return;

  // If the request is for the favicons, fonts, or the built files (which are hashed in the name)
  if (
    url.pathname.startsWith("/favicons/") ||
    url.pathname.startsWith("/fonts/") ||
    url.pathname.startsWith("/build/")
  ) {
    event.respondWith(
      // we will open the assets cache
      caches.open("assets").then(async (cache) => {
        // if the request is cached we will use the cache
        let cacheResponse = await cache.match(event.request);
        if (cacheResponse) return cacheResponse;

        // if it's not cached we will run the fetch, cache it and return it
        // this way the next time this asset it's needed it will load from the cache
        let fetchResponse = await fetch(event.request);
        cache.put(event.request, fetchResponse.clone());

        return fetchResponse;
      })
    );
  }

  return;
});

If you setup i18next in your Remix app you can add this code to the sw.js to cache the localized messages with an stale while revalidate strategy

// add before the return above
// if the URL is for a localized message
if (url.pathname.startsWith("/locales/")) {
  event.respondWith(
    // we will use the locales cache
    caches.open("locales").then(async (cache) => {
      // we will run the fetch
      let fetchResponsePromise = fetch(event.request);
      try {
        // try to read from cache
        let cacheResponse = await cache.match(event.request);
        if (cacheResponse) return cacheResponse;
      } finally {
        // and finally if it was not cached or after we sent the response
        let fetchResponse = await fetchResponsePromise;
        // we will update the cache
        cache.put(event.request, fetchResponse.clone());
        // and return the response
        return fetchResponse;
      }
    })
  );
}

With this code, the first request to (e.g.) /locales/en.json will be requested and cached, the next time the user will receive the cached JSON immediately while the browser fetch and update the cache, the next time it will receive the updated cached JSON and it will follow the flow to update the cached value again.

Registering the Service Worker

Finally, we need to register the Service Worker to be used by the browser. We can do this in the entry.client file.

// if the browser supports SW (all modern browsers do it)
if ("serviceWorker" in navigator) {
  window.addEventListener("load", () => {
    // we will register it after the page complete the load
    navigator.serviceWorker.register("/sw.js");
  });
}

And that's it, except for the entry.client the rest of the code is not actually tied to Remix, and if you used the code above for your SW and go the the Network tab in the devtools you will see, after the first access, all the assets, and maybe locales, will be loading from the Service Worker and the will always load in a few milliseconds, even if you make the network slower, because they are already cached in the browser.