w3c / w3c/ServiceWorker

How should import() work in service workers?

Open
#1,585 20 comments 3 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Bikeshed
Stars
3.6k
Forks
324
Avg merge
14d 22h
Merged PRs (30d)
1

Description

We blocked import() in service workers because we weren't sure how they should work, but it wasn't intended to be a forever-fix. Now that modules are becoming more popular, and browsers now support module service workers, how should import() work?

Some thoughts:

  • It should work the same in classic service workers as it does with module service workers.
  • Ideally it should promote patterns that work offline.
  • Loading of a module in one service worker cannot be impacted by another service worker.

Some ideas:

Other ideas considered

Option 1

Before the service worker is "installed", calls to import() are fetched, bypassing all service workers. These module resources are cached along with the service worker script.

After the service worker is installed, calls to import() will only use resources that are cached along with the service worker script, otherwise they will fail.

Eg:

addEventListener('install', (event) => {
  event.waitUntil((async () => {
    const cache = await caches.open('static-v1');
    return Promise.all([
      cache.addAll(urls),
      import('./big-script.js'),
      import('./another-big-script.js'),
    ]);
  })());
});

addEventListener('fetch', (event) => {
  event.respondWith((async () => {
    if (whatever) {
      const { complicatedThing } = await import('./big-script.js');
      // …etc…
    }
  })());
});
  • ✅ Compatible with classic and module service workers
  • ✅ Promotes working offline
  • ✅ Similar to how importScripts works
  • ✅ Don't need to know the full import tree, except for dynamic imports
  • ⚠️ Requires parsing (and maybe executing) stuff in the install phase that isn't actually used

Another idea:

Option 2

Before the service worker is "activated", calls to import() just go to the network, bypassing all service workers.

After the service worker is "activated", calls to import() will go via that service worker's "fetch" event.

addEventListener('install', (event) => {
  event.waitUntil((async () => {
    const cache = await caches.open('static-v1');
    return cache.addAll([
      ...urls,
      './big-script.js',
      './big-script-dependency.js',
      './another-big-script.js',
      './another-big-script-dependency.js',
    ]);
  })());
});

addEventListener('fetch', (event) => {
  event.respondWith((async () => {
    if (whatever) {
      const { complicatedThing } = await import('./big-script.js');
    }
    return caches.match(event.request);
  })());
});
  • ✅ Compatible with classic and module service workers
  • ✅ Scripts can be cached without parsing them
  • ⚠️ Need to list out all the module's dependencies
  • ⚠️ It can work offline, via the same mechanism as pages, but it can also become network dependent. It's versatile, but folks might not test for it?
  • ⚠️ Implementation complexities? I think it's the first time a request from a service worker will go through its own fetch event.

We could overcome the dependencies issue with something like cache.addModule('./big-script.js'), which would crawl the tree similar to modulepreload. That means we're having to parse the modules, but it doesn't need to execute them.

I preferred "option 2" when it was just in my head, but now I've written it down and thought it through, I don't think I like it. So, what about:

Option 1.5

Before the service worker is "installed", calls to import() fail.

Before the service worker is "installed", calls to installEvent.installModule(url) crawl a module tree similar to modulepreload, and cache the script along with the service worker script. This returns a promise that indicates success. It isn't necessary to pass this to waitUntil, but it doesn't hurt to do so.

After the service worker is "installed", calls to import() will only use resources that are cached along with the service worker script, otherwise they will fail.

After the service worker is "installed", calls to installEvent.installModule(url) reject.

Eg:

addEventListener('install', (event) => {
  event.installModule('./big-script.js');
  event.installModule('./another-big-script.js');
  event.waitUntil(
    caches.open('static-v1').then(c => c.addAll(urls)),
  );
});

addEventListener('fetch', (event) => {
  event.respondWith((async () => {
    if (whatever) {
      const { complicatedThing } = await import('./big-script.js');
      // …etc…
    }
  })());
});
  • ✅ Compatible with classic and module service workers
  • ✅ Promotes working offline
  • ✅ Don't need to know the full import tree, except for dynamic imports
  • ✅ Requires parsing the scripts at install time, but avoids executing them

Thoughts @wanderview @asutherland @jeffposnick @mfalken @youennf?

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the proposed import() behavior in service workers, especially the install and fetch event examples and installEvent.installModule(). Read the discussion and compare the outlined options before making changes. Done means the project reaches agreement on behavior and records the resulting specification direction; no implementation files or tests are named.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
web-dev
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.