A modern alternative to importScripts().
Background
ES modules have been a developer favorite for a while now. In addition to a number of other benefits, they offer the promise of a universal module format where shared code can be released once and run in browsers and in alternative runtimes like Node.js. While all modern browsers offer some ES module support, they don't all offer support everywhere that code can be run. Specifically, support for importing ES modules inside of a browser's service worker is just starting to become more widely available.
This article details the current state of ES module support in service workers across common browsers, along with some gotchas to avoid, and best practices for shipping backwards-compatible service worker code.
Use cases
The ideal use case for ES modules inside of service workers is for loading a modern library or configuration code that's shared with other runtimes that support ES modules.
Attempting to share code in this way prior to ES modules entailed using older "universal" module formats like UMD that include unneeded boilerplate, and writing code that made changes to globally exposed variables.
Scripts imported via ES modules can trigger the service worker
update
flow if their contents change, matching the
behavior
of
importScripts()
.
Current limitations
Static imports only
ES modules can be imported in one of two ways: either
statically,
using the import ... from '...'
syntax, or
dynamically,
using the import()
method. Inside of a service worker, only the static
syntax is currently supported.
This limitation is analogous to a
similar restriction
placed on importScripts()
usage. Dynamic calls to importScripts()
do not
work inside of a service worker, and all importScripts()
calls, which are
inherently synchronous, must complete before the service worker completes its
install
phase. This restriction ensures that the browser knows about, and is
able to implicitly cache, all JavaScript code needed for a service worker's
implementation during installation.
Eventually, this restriction might be lifted, and dynamic ES module imports may be allowed. For now, ensure that you only use the static syntax inside of a service worker.
What about other workers?
Support for
ES modules in "dedicated" workers—those
constructed with new Worker('...', {type: 'module'})
—is more widespread, and
has been supported in Chrome and Edge since
version 80, as well as
recent versions of Safari.
Both static and dynamic ES module imports are supported in dedicated workers.
Chrome and Edge have supported ES modules in shared workers since version 83, but no other browser offers support at this time.
No support for import maps
Import maps allow runtime environments to rewrite module specifiers, to, for example, prepend the URL of a preferred CDN from which the ES modules can be loaded.
While Chrome and Edge version 89 and above support import maps, they currently cannot be used with service workers.
Browser support
ES modules in service workers are supported in Chrome and Edge starting with version 91.
Safari added support in the Technology Preview 122 Release, and developers should expect to see this functionality released in the stable version of Safari in the future.
Example code
This is a basic example of using a shared ES module in a web app's window
context, while also registering a service worker that uses the same ES module:
// Inside config.js:
export const cacheName = 'my-cache';
// Inside your web app:
<script type="module">
import {cacheName} from './config.js';
// Do something with cacheName.
await navigator.serviceWorker.register('es-module-sw.js', {
type: 'module',
});
</script>
// Inside es-module-sw.js:
import {cacheName} from './config.js';
self.addEventListener('install', (event) => {
event.waitUntil((async () => {
const cache = await caches.open(cacheName);
// ...
})());
});
Backwards compatibility
The above example would work fine if all browsers supported ES modules in service workers, but as of this writing, that's not the case.
To accommodate browsers that don't have built-in support, you can run your
service worker script through an
ES module-compatible bundler to create a
service worker that includes all of the module code inline, and will work in
older browsers. Alternatively, if the modules you're attempting to import are
already available bundled in
IIFE or
UMD formats, you can import them using
importScripts()
.
Once you have two versions of your service worker available—one that uses ES modules, and the other that doesn't—you'll need to detect what the current browser supports, and register the corresponding service worker script. The best practices for detecting support are currently in flux, but you can follow the discussion in this GitHub issue for recommendations.
_Photo by Vlado Paunovic on Unsplash_