On my train journey way back home, I was amazed over the number of apps I have in my phone and that, I’ve different apps to serve my disparate demands. Living in an internet age and I, being a Web Developer, who is highly obsessed with the word ‘Web’ in my title, felt kind of disappointed and immediately switched back to my browser to navigate to the website, to read the blog that I was peacefully reading on the native app before a weird kind of feeling popped in. To my surprise, it’s been 50 seconds and all I can see is the loading icon, slowly some content started appearing on the page. While I was watching over the trees outside the train and waiting patiently for the site to breath, images started appearing one by one and after it felt like ages, I can feel, my browser sighed after having done a lot of hard work communicating back & fro to the server to get all the needed resources. I can easily feel the pain, that the users go through while browsing their favorite content on web and completely understand the urge to switch back to the native app experience.
Native apps leverage the power of operating systems. Besides performance, these apps come with many incredible features like Background Sync, Offline-First, Push Notifications. We’re such a powerful community of JavaScript Developers, we can’t let this experience haunt us for, we couldn’t devise a solution at-least as powerful as native apps. Seems like, someone has already taken it seriously and there we have, The Service Worker! Service Worker provides a rich offline-first user experience, background sync, seamless handling for network connectivity gotchas — no internet or weak signal and push-notifications as well.
Let’s try to understand what is a service worker
Service Worker, in simple terms, is a script file that runs in the background without interfering with the user interactions. Service Worker acts as a proxy server that intercepts the network requests sent by your web application to the server. In the sense, requests to fetch javascript or css files, images goes through service worker to the server. Service Worker has the ability to modify this request or send a custom response back to the client. It uses caching to its full advantage to cache some of the resources, so that subsequent requests for these resources can be met out speedily with a cache hit instead of going all the way back to the server to fetch it (More on caching later). Service Worker operates as an event-driven system.
Event-Driven
Service worker goes to sleep to conserve memory when it is not needed. So data is not saved in global state in onfetch & onmessage handlers. To persist data across restarts, you can make use of IndexedDb API.
Let’s check a simple example demonstrating the above statement -
UseCase — We have to preserve product Ids against the number of times a particular product has been viewed by the user. We can store this in a hashmap like -
Since we cannot rely on global variables, we’d be storing this information in IndexedDb so that it is preserved.
Below is a fetch handler for intercepting network requests. If the request url contains ‘product’, we fetch the existing product count stored against the productId from IndexedDb and increment the productCount by 1 and store it back in IndexedDb.
Communication with other web pages
Service Worker cannot access the DOM directly. It works in a different context and can communicate with the client using postmessages.
UseCase — We’ve to send a message to the client when the service worker gets activated.
Below is the handler for service worker activate event. Service worker sends a post message to the client on activation.
Below is the listener on the client javascript that listens to messages and accordingly performs an action when the message is from service worker.
Service Worker works only on HTTPS sites
Service Worker’s onfetch event handler can alter or fabricate the response of the network requests. So it is very important to make sure that the service worker hasn’t tampered on its journey through the network. Also, the service worker should be served from the same domain.
Isn’t this all exciting? Let's dive in to understand how can we include a service worker in our web application.
1. Registration
To let service worker control your web pages, we first have to register it against a scope.
Include the below snippet in your client JavaScript to register the service worker script ‘service-worker.js’
There are some old browsers that don’t support service worker and so, before registering it we first check if service worker API is supported in the browser. The service worker register function asynchronously downloads the script. If the service worker is registered successfully, it kicks off the installation step.
When your page is refreshed, the service worker registration code is not executed again. You don’t have to worry about preventing your registration code from executing multiple times, the browser takes care of that. Service Worker would be registered again only if the URL of the service worker is different or there is a byte difference between the registered service worker and the newer instance of it.
If the registration fails, the browser tries it the next time you reload the page.
Scope of a Service Worker
By default, the scope of the service worker is ‘/’, which means it can intercept all the network requests coming from the root. To register a service worker against a different scope or restrict its capability to intercept requests only from a few selected pages, we can do something like this -
The register function takes in a second optional parameter of type object, that has scope as one the keys. So, the scope of the service worker is now restricted to ‘/products/fashion/{…}’. It cannot intercept any request that is beyond its scope.
2. Installation
Once the service worker is registered successfully on the client, all the succeeding steps are a part of the service worker script. You can perform the initialization tasks here like setting up the browser cache or creating object stores in IndexedDB
Below is the snippet for adding URLs to the cache
If all the URLs/files are cached successfully, then the service worker is ready to enter into the next step. But if even one of the files fails, it throws an error in the installation step and will be installed, the next time page is refreshed.
3. Activation
After successful installation, service worker enters in the ready state. If there is no active service worker, the installed service worker enters into the activation phase. Here, we can manage the old cache resources or clean up the stale data in IndexedDb.
In the activate event handler above, we’re deleting all the caches except ‘products-v2’. Even if the activation step is done successfully, the service worker still doesn’t control the pages. The network requests made post activation will still not go through service worker’s onfetch handler. Service worker would reign over the website when the website is not opened in any other tab and the current page is refreshed. Service worker will listen to network request on a web page only if, the page itself was fetched through service worker. This does make sense because it is possible that the web page is using some resources from the cache and the service worker has modified the cache’s resources in the activation event. If you still want your service worker to intercept the subsequent network requests, you can use clients.claim(). clients.claim() lets you override this default behavior and allows service worker to intercept network requests immediately on successful activation.
But if there was already an active service worker on the website, the new service worker won’t enter into the activation phase unless this old service worker is unregistered.
If you want to get rid of active service worker and let the new one take over, you can do something like this —
self.skipWaiting() prevents the waiting state and activates the service worker as soon as it is installed.
Let’s try to understand the terminologies that we’ve used in our code
What’s with the ‘self’ keyword?
self is used as a service worker reference like we use ‘this’ to refer any instance in JavaScript.
And what about event.waitUntil() Why are we even using it?
By now, we know that service worker operates as an Event-driven system. It goes into a dormant state when it is not needed. Until the code written in the install or activate events is executed, we would want our service worker to be alive and do the working. event.waitUntil() does exactly that. Service Worker remains active until the code inside event.waitUntil is executed.
How do I know if my web page is controlled by any service worker?
Check out my blog illustrating the use of Service Workers in building Offline-first applications.