Tweaks to cache.addAll() and importScripts() coming in Chrome 71

Developers using service workers and the Cache Storage API should be on the lookout for two small changes rolling out in Chrome 71. Both changes bring Chrome's implementation more in line with specifications and other browsers.

Disallowing asynchronous importScripts()

importScripts() tells your main service worker script to pause its current execution, download additional code from a given URL, and run it to completion in the current global scope. Once that's done, the main service worker script resumes execution. importScripts() comes in handy when you want to break your main service worker script into smaller pieces for organizational reasons, or pull in third-party code to add functionality to your service worker.

Browsers attempt to mitigate the possible performance gotchas of "download and run some synchronous code" by automatically caching anything pulled in via importScripts(), meaning that after the initial download, there's very little overhead involved in executing the imported code.

For that to work, though, the browser needs to know that there won't be any "surprise" code imported into the service worker after the initial installation. As per the service worker specification, calling importScripts() is supposed to only work during the synchronous execution of the top-level service worker script, or if needed, asynchronously inside of the install handler.

Prior to Chrome 71, calling importScripts() asynchronously outside of the install handler would work. Starting with Chrome 71, those calls throw a runtime exception (unless the same URL was previously imported in an install handler), matching the behavior in other browsers.

Instead of code like this:

// This only works in Chrome 70 and below.
self.addEventListener('fetch', event => {
  importScripts('my-fetch-logic.js');
  event.respondWith(self.customFetchLogic(event));
});

Your service worker code should look like:

// Move the importScripts() to the top-level scope.
// (Alternatively, import the same URL in the install handler.)
importScripts('my-fetch-logic.js');
self.addEventListener('fetch', event => {
  event.respondWith(self.customFetchLogic(event));
});

Deprecating repeated URLs passed to cache.addAll()

If you're using the Cache Storage API alongside of a service worker, there's another small change in Chrome 71 to align with the relevant specification. When the same URL is passed in multiple times to a single call to cache.addAll(), the specification says that the promise returned by the call should reject.

Prior to Chrome 71, that was not detected, and the duplicate URLs would effectively be ignored.

A screenshot of the warning message in Chrome's console
Starting in Chrome 71, you'll see a warning message logged to the console.

This logging is a prelude to Chrome 72, where instead of just a logged warning, duplicate URLs will lead to cache.addAll() rejecting. If you're calling cache.addAll() as part of a promise chain passed to InstallEvent.waitUntil(), as is common practice, that rejection might cause your service worker to fail to install.

Here's how you might run into trouble:

const urlsToCache = [
  '/index.html',
  '/main.css',
  '/app.js',
  '/index.html', // Oops! This is listed twice and should be removed.
];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('my-cache').then(cache => cache.addAll(urlsToCache))
  );
});

This restriction only applies to the actual URLs being passed to cache.addAll(), and caching what ends up being two equivalent responses that have different URLs—like '/' and '/index.html'—will not trigger a rejection.

Test your service worker implementation widely

Service workers are widely implemented across all major "evergreen" browsers at this point. If you regularly test your progressive web app against a number of browsers, or if you have a significant number of users who don't use Chrome, then chances are you've already detected the inconsistency and updated your code. But on the off chance that you haven't noticed this behavior in other browsers, we wanted to call out the change before switching Chrome's behavior.