w3c / ServiceWorker

Service Workers
https://w3c.github.io/ServiceWorker/
Other
3.63k stars 315 forks source link

Feature detection for type="module" support #1582

Open jeffposnick opened 3 years ago

jeffposnick commented 3 years ago

I've recently been experimenting with ES modules for service workers, now supported in pre-stable versions of Chrome and Safari.

I've found that without a way to feature detect support, writing code that's backwards compatible can be awkward, and in some browsers, less performant.

Roughly, this is the registration snippet that I was trying out:

async function registerSW() {
  try {
    await navigator.serviceWorker.register('es-module-sw.js', {
      type: 'module',
    });
  } catch(error) {
    await navigator.serviceWorker.register('import-scripts-sw.js');
  }
}

Pre-stable versions of Chrome and Safari will successfully run the first registration, and the current stable versions of Chrome will raise an exception immediately because it "knows" that type: 'module' isn't supported.

However, the current stable versions of Firefox and Safari will download and attempt to execute the ES module flavor of service worker, and only raise an exception when there's a syntax error due to the usage of ES module imports. The fallback logic in the catch can successfully register a classic version of the service worker that uses importScripts(), but not until after spending the bandwidth and time needed to download and parse something that we should know in advance won't work.

Am I missing something already implemented in all browsers that would make it possible to feature-detect type: 'module' support ahead of time? If not, consider this a feature request to add in a property, presumably exposed on the ServiceWorkerContainer, that could answer the "are ES module service workers supported?" question in advance of attempting the registration. Either a boolean that was true when ES modules are supported, or alternatively, a list of supported type values, set in this case to ['classic', 'module'], if we think that there might be additional types that will be added in the future.

jakearchibald commented 3 years ago

A workaround for now:

let readType = false;
navigator.serviceWorker
  .register('about:blank', {
    get type() {
      readType = true;
    },
  })
  .catch(() => {});

If readType is false, it doesn't understand type.

jeffposnick commented 3 years ago

While that does detect type support in general, it isn't sufficient to detect type: 'module' support. Both Chrome 90 and Safari 14.0.3 support type but don't work with type: 'module'.

In the case of Chrome 90, there's an exception throw immediately (DOMException: type 'module' in RegistrationOptions is not implemented yet.See https://crbug.com/824647 for details.), so it does avoid downloading and parsing the script.

In the case of Safari 14.0.3, the ES module version of the script downloads and then fails to parse, with a TypeError: SyntaxError: Unexpected token '*'. import call expects exactly one argument., so checking for type doesn't avoid the problem. I'm assuming this is because Safari 14.0.3 shipped with partially implemented ES module support, as the Technology Previews post-122 work as expected.

jakearchibald commented 3 years ago

sighhhhh that Safari issue is frustrating.

jakearchibald commented 3 years ago

https://static-misc-2.glitch.me/detect-sw-module-support/ - here's a test that works today, although it requires a network request.

jeffposnick commented 3 years ago

Epic 😄 It's good to have that to refer folks to if they want to use ES modules today and also want to avoid a potential double-download. Long-term, is a more official mechanism for detecting support reasonable to add to the service worker specification?

Actually, since ES modules in dedicated workers are already supported in Chrome and Safari, how do developers using them deal with what I'm assuming is a similar issue? ~Perhaps a way of feature-detecting support needs to be done on the WorkerGlobalScope level, to address that use case as well?~

(EDIT: Although I guess code in the window scope can't interrogate the WorkerGlobalScope, so that probably wouldn't help.)

jakearchibald commented 3 years ago

Perhaps a way of feature-detecting support needs to be done on the WorkerGlobalScope level, to address that use case as well?

It'd be nice to be able to detect it without starting up a worker. Although, at least with a worker you can avoid the network request. But yeah, it'd be good to have the same solution in both places.

Going to see if I can make the test better, so it might be broken for a bit…

wanderview commented 3 years ago

Maybe not a great general solution, but would there be any benefit in changing the Service-Worker: script header to say Service-Worker: module when type is module and its supported? That way the server could dynamically choose to return either a classic or module script.

jakearchibald commented 3 years ago

Going to see if I can make the test better, so it might be broken for a bit…

I couldn't make it better. I thought Safari might support the type option but ignore the value. But no, it validates it (I guess they have the IDL), but doesn't do the right thing with the value.

@wanderview

Maybe not a great general solution, but would there be any benefit in changing the Service-Worker: script header to say Service-Worker: module when type is module and its supported?

That seems nice!

jeffposnick commented 3 years ago

If https://github.com/w3c/ServiceWorker/issues/1585 is resolved, and dynamic import() comes to service workers, it would introduce one more capability that would be important to know prior to service worker registration.

You could theoretically create four different versions of a service worker script—importScripts() vs. static module imports, and dynamic import()s vs. no dynamic support—to cover all the possible variations, and registering them one at a time until you find the one that doesn't throw isn't a good model. (Though I doubt it's likely that a browser will support dynamic import() but not static module imports...)

Exposing some combination of ['classic', 'dynamic', 'static'] as a new importSupport property on the ServiceWorkerContainer, reflecting the set of capabilities of the current browser, would address this expanded use case. Browsers that don't have an importSupport property on ServiceWorkerContainer could be assumed to only support classic importScripts().

wanderview commented 3 years ago

Maybe not a great general solution, but would there be any benefit in changing the Service-Worker: script header to say Service-Worker: module when type is module and its supported?

That seems nice!

Should we pursue this?

jakearchibald commented 3 years ago

I think a client-side solution would be a more general solution. Might not be worth spending time on the header thing yet?