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.