On this blog, I already wrote a few articles about Progressive Web Apps (PWAs) — including a full tutorial that walks you through converting your React app into a PWA. Because Service Workers are such an important part of PWAs, I found myself explaining what they are over and over again.
This article is an introduction to the Service Worker API of JavaScript, walking you through the registration of a Service Worker that will serve some content from the cache.
Watch my PWA online course 📹
You can now watch my course on how to turn your React app into a PWA on Skillshare. Learn what you need to know to get started, and turn your React app into a PWA in just 10 minutes.
Watch it for free with my referral link, which will give you a 14 day trial.
What are Service Workers?
A Service Worker is a scriptable network proxy in a web browser that manages network requests for a webpage. — Wikipedia
Service Workers enable a set of features that previously were exclusive to native applications. The first draft for Service Workers has been published in 2014 and now they are supported by all major browsers.
Like the definition already pointed out, Service Workers are network proxies. This means that that they can control all network requests from the page and they can be programmed to respond with a cached response.
Just like Web Workers, their code is executed in a separate thread, which allows them to be active even though all tabs of the webpage are closed. The browser, however, will terminate and re-activate them depending on whether they are being used or not.
The Service Worker API relies heavily on promises, which is why it's recommended to learn about them first. A good point to start if you're new to promises is Google's developer blog.
How can I register a Service Worker?
Registering a Service Worker doesn't involve much code at all. All you need is a JavaScript file
for the Service Worker code (I'll call it service-worker.js
) and a place in your application where you'll register it.
In my example, service-worker.js
is located at the root of the project and hosted under the same domain.
// We first check if the browser supports Service Workers
if ('serviceWorker' in navigator) {
navigator.serviceWorker .register('/service-worker.js') .then(function(registration) {
console.log(registration);
})
.catch(function(err) {
console.log(err);
});
}
What features do Service Workers enable?
In this section, I'll go a little more into detail about the features of Service Workers, including some small code examples.
Service Workers enable the following features, which are the core of Progressive Web Apps:
- Offline capabilities
- Periodic background syncs
- Push notifications
Offline capabilities
Since this is the main selling point of Service Workers, I'll go a little more into the details of how this works.
Service Workers provide offline capabilities by caching resources and intercepting network requests, which can be served with the previously cached resources instead of requesting the server.
We can derive two steps from this:
- Precaching
- Serving requests from the cache
Both of these steps make use of the Cache API, which can be used by Web Workers and the browser and which provides us with a storage mechanism for network requests.
Access to localStorage is blocked for the context of Web- and Service Workers to prevent concurrency issues. As an alternative, IndexedDB can be used for storing larger amounts of data.
Precaching
Precaching is a term that describes downloading and caching files before the Service Worker becomes active. It's done during the "Installing" step of the Service Worker's lifecycle. Once the Service Worker has become active, it will then be ready to serve the files from the cache.
I wrote an entire article on the Service Worker lifecycle, which is why I won't go deeper on this topic right now.
Usually, we want to cache the Application Shell, which is the minimal amount of code required to run your website. It's the bundle of code that you would upload to an app store if you developed a native application. This includes all the basic JavaScript, HTML, and pictures necessary.
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(currentCache.offline).then(function(cache) {
return cache.addAll([
'/static/images/offline.svg',
'/static/html/offline.html',
]);
});
);
});
What about bundled files?
You might not always know the name of the files you want to precache because you are using a bundler that auto-generates files with hashes in their names (like Webpack or Rollup for example).
For those cases, you can use Workbox, which provides tools that integrate into your build and automatically create a precaching list for you (check out the Workbox plugin for Rollup and Webpack).
Serving requests from the cache
At this stage we already have all our application code stored in the cache and the Service Worker has become active and is controlling the page.
Now the only thing missing is listening to the fetch
event and returning results from the cache.
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});
In this case, we respond with the cached content whenever possible. As a fallback, we make a network request.
This is called a cache first strategy. There are a few others, like network first, cache only and network only. The names are already pretty self-explaining but if you want to read more about it you can read more on Google's developer blog.
(Periodic) background syncs
As I already mentioned in the introduction, Service Workers run on a separate thread from the other Service Workers and they can even execute their code when the page is closed. This ability is important for doing background synchronizations and providing push notifications.
I put "periodic" in parentheses because background syncs and periodic background syncs are not quite the same. So let's have a look at what they can do:
Background syncs
Background syncs are usually used for synchronizing data after the user navigated away from the page.
As an example, after editing a document on your phone you will hit "save" and leave the page. If your internet connection drops during editing the document, you would have to wait for the connection to come back to save the document.
Background syncs intend to solve this problem by automatically sending the data once the connection is re-established.
Instead of actively waiting for the connection to come back, your UI can give immediate feedback that the changes will be saved in the background and you can put the phone back without losing time.
Let's have a look at a code example:
navigator.serviceWorker.ready.then((registration) => {
return registration.sync.register('sync-save-document');
});
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-save-document') {
event.waitUntil(saveDocument());
}
});
saveDocument
is a function that returns a promise that can either be resolved or rejected. If it's being rejected — due to
network issues for example — the sync will automatically try again.
One important thing to note is that the sync tags have to be unique. If I want to schedule 5 background syncs of the type "message" for example, only the last one will go through. That's why in this case, each tag should have a unique identifier.
Periodic background syncs
Periodic background syncs are supposed to solve a different problem than normal background syncs. This API can be used for updating data in the background without having to wait for any interaction of the user.
This can be useful for a broad range of apps. With this technology, the user might be able to read the latest news articles without having an internet connection.
To prevent websites from abusing this, the frequency of those synchronizations depends on the site-engagement score, which the browser sets for every website. If you open a specific web app a lot, this frequency can go up to a maximum of 12 hours.
Another requirement for this to work is that the website is installed and added to the home screen as a Progressive Web App on mobile devices.
At the time of writing this article, the periodicSync
API is only supported by Chrome (v80) and Edge and thus not ready for
production use.
Push notifications
Another native-like feature that Service Workers enable is push-notifications. We usually know them from our phones in the form of messages or social media notifications, but they are also available on desktop computers.
They are supported on all major browsers except Safari, which has its own implementation for the desktop app.
To use push notifications, you need to set up a server, that will be pushing the notification to all clients. Since the Service Worker runs in the background on a different thread, the user will be able to see push notifications even though the page isn't currently open.
The implementation is a little more complicated and worth of an article on its own, that's why I won't include any code examples here. You can check out this introduction if you are curious.
Service Worker Browser Support
Nowadays, Service Workers are widely supported and ready to be used in production. Features like periodic background sync aren't ready yet but the core, which is providing a rich offline experience, is supported by all modern browsers.
It's good to remember that Service Workers are part of Progressive Web Apps, where progressive means enhancing functionality. This means that even though parts of it won't work in all browsers, your website should still be working when Service Workers aren't supported at all.
In addition to the screenshot above, which is automatically updated, you can check the page isserviceworkerready.
Conclusion
I hope I could give you a better understanding of Service Workers by walking you through the basic concepts and features. If you want to get notified about my latest articles, go ahead and subscribe to my newsletter.
Any open questions? Send me a message on Twitter or via Email and I'll make sure to answer it!