Service workers are a core enabling technology for modern web apps. They enable background push, offline mode, and cached assets. I’ll be covering those in future articles, but for now, let’s look at the service worker first. Service workers go through the following stages:
The website is responsible for letting the browser know what JavaScript to load as a service worker.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function registrerServiceWorker() {
if (!navigator.serviceWorker) {
return false
}
try {
const registration = await navigator.serviceWorker.register("/my-code.js")
console.log("Service worker is registered")
return registration
} catch(e) {
console.error("Failed to register service worker", e)
return false
}
}
The code provides a single JavaScript file which it will load and install. This file can be named anything, but it needs to be in the root folder to avoid scope issues. It’s worth noting that most browsers will ignore caching headers for this file. The browser will do a byte comparison of the files and update the service worker if needed.
The service worker is only able to be active for pages with the same file path. This means that a page located at /foo/bar.html
will NOT be affected if a service worker is loaded from /foo/bar/baz/service_worker.js
since it is located in the bar subfolder. This is done for security reasons, operating under the assumption that files in subfolders may not have the same privileges as files higher in the folder structure. An example of where this would be an issue would be a compromised WordPress install located at /blog
injecting a malicious service worker into the main website.
For Rails, this can be a problem since Rails serves frontend resources from /assets/*
by default. In Rails 8, the default is to serve a single asset template file, but that removes the template from the normal asset pipeline. I like re-serving a file from the normal pipeline as another solution to this problem.
Once a service worker has been registered it enters an installation and waiting phase. During this phase, the service worker doesn’t control the page but can make preparations. This can look like downloading data for offline usage or caching files for faster local use. Once the service worker enters this phase, it will receive an install
event:
1
2
3
4
5
6
7
self.addEventListener("install" event => {
console.log("Entered install phase for service worker")
event.waitUntil(
// cache files etc
)
})
Service worker gotcha #2 is that when the service worker is done successfully installing, it stays in memory but does not take over executing. This is done since any of the open pages might be reliant on the previous service worker. Once the old service worker isn’t being used by any pages, then the new service worker will become the active service worker.
To be clear, this means if your web app typically has tabs that remain open for a long time (SPA) you will possibly need to have something in place to kick off a registration. This would look something like:
1
2
3
navigator.serviceWorker.ready.then(registration => {
registration.update()
})
Your service worker will receive an activate
event once it becomes the controlling service worker. It is common to clean up after old service workers, for example, clearing old caches.