There’s a fairly well known quote in the development world:
There are only two hard things in Computer Science: cache invalidation and naming things.
This might have filled me with fear as I recently began building a dashboard for displaying gaming data. The intention was for the project to be a Progressive Web Application (referred to as PWAs hereafter), and that included an element of caching.
As a front-end developer, caching is not a problem that I’d had to solve before, but as it turned out, it was an interesting one, and (surprisingly) not nearly as difficult as I had been led to believe.
But I may be getting ahead of myself here…
What is a Progressive Web App?
PWAs have been around for a while now, but for those of you that haven’t heard of them, the idea is that they use bleeding edge Javascript to simulate the ‘native’ experience for your run-of-the-mill website.
In order to be considered a PWA, a website needs to be:
-
fast (it should load immediately),
-
reliable (not dependant on the network),
-
and app-like (...it should look like an app).
We could spend a lot more time talking about what makes a PWA and what they should do, but that short list covers the main points.
It’s really the first two that we’re concerned with here, and, as it happens, they are the two that presented the biggest challenges for our dashboard. Making your app super-fast might be thought of as a performance consideration, and offline availability… Well, it’s not something that we would have associated with a website in the past.
But this is a brave new world, led by Javascript and it’s newfound spirit of adventure! PWAs solve our speed and reliability woes by letting us cache our content locally. “But how?” I hear you ask. Let me tell you.
How PWAs Do Caching
The real star of the show is another bit of tech called the Service Worker. PWAs rely heavily on Service Workers for a lot of their core functionality; I like to imagine them as little dudes that sit next to your website, catching all of it’s network requests and then subsequently doing something with them. In other words, they act as a proxy.
They run in a separate thread from the ‘normal’ Javascript, which means they’re slightly divorced from what’s actually going on on the page. However, Service Workers can interact with the page through some of the newer APIs that have appeared in the HTML specification (more on that soon).
So we have our Service Worker, catching our network requests, but that’s not the end of the story. Once we have a request, we need to do something with it. And once we’ve done something, presumably we’ll have some data that we want to put… somewhere. And that brings us back to our original quote, where even after we’ve managed to cache something, we need to figure out how to invalidate that cache.
Luckily, Workbox leapt to my rescue.
Workbox
Workbox is a set of libraries produced by those clever folk at Google, aimed at simplifying caching. Specifically it helps developers take advantage of the possibilities provided by Progressive Web Apps.
And it was a pleasure to work with; having decided on the strategy (to use their terminology) you simply have to register the routes you want to cache in your Service Worker (identifying them via a regular expression).
A strategy is essentially a plan for how the app should obtain data and/or assets; you might want to get data from the network first, or maybe the cache first, or maybe you only want data from the network. Whatever the case, there’s a strategy provided which abstracts the actual detail of actually dealing with a cache.
In the below example, you can see two routes registered; one is a catch-all for site assets, and the other is for one of the API endpoints. Each one receives its own unique name, so you can identify it later on. This data is then stored in your browser’s cache using the Cache API from the HTML spec.
workbox.routing.registerRoute(
new RegExp("/"),
workbox.strategies.staleWhileRevalidate({
cacheName: "project-assets",
plugins: [new workbox.broadcastUpdate.Plugin("assets-update")]
})
);
workbox.routing.registerRoute(
new RegExp("https://project-api.com/api/stuff"),
workbox.strategies.staleWhileRevalidate({
cacheName: "project-data",
plugins: [new workbox.broadcastUpdate.Plugin("data-update")]
})
);
Finally we make use of the broadcastUpdate plugin, which exposes the BroadcastChannel API. The idea is that you want to let the user’s browser know that the cache has changed. This typically would be difficult because, as we’ve mentioned, Service Workers live in a different browser context. The BroadcastChannel API helps overcome this as long as the scripts/contexts are on the same-origin.
Broadcasting
Workbox’s broadcastUpdate plugin only provides one side of the broadcasting; actually listening for the update is relatively straightforward though. An event type of “message” can be listened for, which gives you the name of the cache and the URL for the original request.
It’s worth noting that, at this point, if there are any new assets and/or data, Workbox has already updated your cache with them. With the information from the event in your hands, you can pull the content out of your cache (again, using the cache API), and then you just have to decide how to update the front-end.
In my case, I provided a button for reloading the page (for the new assets), and a mechanism to get the updated data into the application’s state, as shown in the below example.
const dataUpdatesChannel = new BroadcastChannel("data-update");
dataUpdatesChannel.addEventListener("message", async event => {
const { cacheName, updatedUrl } = event.data.payload;
const newData = await caches.open(cacheName).then(async function(cache) {
return await cache.match(updatedUrl).then(function(response) {
return response.json().then(function(json) {
return json;
});
});
});
window.pushNewAppData(newData);
});
const assetsUpdatesChannel = new BroadcastChannel("assets-update");
assetsUpdatesChannel.addEventListener("message", async event => {
window.hasNewAssets();
});
Summary
And that was that. With these relatively simple bits of code in place, the app loads super-fast, and (on subsequent use) is available offline.
‘Solving’ caching for this little dashboard ended up being one of the most pleasurable parts of the build, cementing Workbox as a tool that I would not hesitate to use again!
Previously from our Engineering Team:
Rails 6: Seeing Action Text in... action by Stephen Giles
A Quick Comment on Git Stash by Karen Fielding
Avoiding N+1 queries in Rails GraphQL APIs by Andy West