Merging Multiple Service Worker Scripts In The Same Scope

Ziyad Mohammed
Tengio Ltd
Published in
4 min readSep 9, 2019
Photo by rawpixel.com from Pexels

Progressive Web Apps are all the rage right now and with good reason. Performance is becoming increasingly important with research showing that a good chunk of users abandon websites if they fail to load in less than 3 seconds. With this in mind, more and more developers are leveraging Service Workers to minimise load times.

This post discusses how to use a lesser-known feature of Service Workers: Merging multiple scripts to share the same scope.

Why do I need more than one script?

I was recently asked to cut down load times using frontend optimisation techniques. Since we were already employing the usual techniques like bundling and minification, my task was to implement asset caching and improve the user experience on slow or unreliable networks.

I started working on it when I discovered the app was using a third-party plugin for sending push messages. The maintainers of the codebase didn’t realise that they were already using a Service Worker under the hood!

Since removing the plugin was not an option, I needed to write a separate Worker script.

The Problem

Since both Service Workers(the plugin and my custom script) needed to be registered at the root URL of the website, the third-party plugin was overriding my custom script and rendering it useless.

The more I learned about Service Workers, the more convinced I became that my script could not coexist with the third-party application. Reason?

If two Service Worker scripts have the same scope path and even a byte of difference, the browser is going to consider them two versions of the same SW and install the latest version in the background. [Angular University]

Each Service Worker NEEDS TO HAVE A UNIQUE SCOPE.

Solution: importScripts To The Rescue

According to MDN,

The importScripts() method of the WorkerGlobalScope interface synchronously imports one or more scripts into the worker's scope.

Unfortunately, that’s about it. The MDN page does not go into details of how this extremely handy function actually works.

What importScripts() does is:
It fetches the code from the provided scripts and then registers 1 Service Worker which consists of the aggregated code from all of the imported scripts.

The great thing about importScripts is that you can even import scripts hosted on another domain or a CDN.

Caveats

There are a few things you should keep in mind when working with multiple scripts.

Modules Are Not Supported

importScripts only works with scripts, not modules. Using module.exports in the script being imported will throw an error.

Different Files, Same Context of Execution

You can’t use the same variable name at the root level in different scripts which are imported. This will definitely lead to unexpected behaviour and possibly errors. This is because all imported scripts are running in the same context of execution: the worker global scope.

As you might have guessed, reversing the order of params in the call to importScripts() will result in a different output.

importScripts('sw-2.js, sw-1.js');
console.log(currentCacheName); //Output: cache:v1

Updating The Service Worker In The Browser

Making changes in the imported scripts will not trigger an update to the Service Worker. Only changes to the root level worker will be detected by the browser. This means cache-versioning has to be done in the root level worker.
Let me explain using the same example.

Any changes made to sw-1.js or sw-2.js will not trigger an update. The user will still be stuck with the old Service Worker code and the changes will not reflect. To ensure that the latest code is served and a new Worker is registered, we need to make a change in root-service-worker.js .

My preferred way of handling this is using versioning for the Service Worker. Since changing the version has nothing to do with the behaviour of the Service Worker, we need to make sure we don’t introduce any bugs unwittingly. I like to add a comment at the top of root-service-worker.js like so:

/* Bump the service worker version in this comment to make sure changes to imported scripts are reflected on the client/browser.SERVICE WORKER VERSION = 1*/ 

Any change made to the root worker will cause the browser to recognise that there is an updated worker available. Even a change inside a comment!

NOTE: This is just one way to ensure that the latest updates are served. You can use any other technique that you see fit. The only requirement is that some change is made to the root file.

There seems to be an automated way of doing so as explained here.
Even though passing { updateViaCache: ‘none’ } as the second parameter to navigator.serviceWorker.register is the suggested way to tackle this issue, this did not work for me [ Chrome Version: 75.0.3770.100 (Official Build) (64-bit) ]

There should be a way to handle this automatically, without the need of manual intervention. Once I figure it out, I’ll update the article. Suggestions welcome!

This post outlines the problems I faced when working with importScripts and aims to address them. If I have missed something, or if you have any suggestions on how to improve this, please write to me at ziyad@tengio.com . I love hearing from my readers :)

EDIT: You can find the source code here.

--

--