New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

<nomodule>, or other patterns for loading both classic and module scripts? #1442

Closed
domenic opened this Issue Jun 20, 2016 · 16 comments

Comments

6 participants
@domenic
Copy link
Member

domenic commented Jun 20, 2016

At BlinkOn @paulirish and a Facebook dev (Paul, do you have his GitHub handle?) were asking about how to ship down both classic scripts for old browsers and module scripts for new browsers. The idea is to ship pure ES6 code with a module graph for new browsers, and a transpiled bundle of code for older ones.

One idea here is to introduce <nomodule> element (analogous to <noscript>). Then you would write:

<script type="module" src="app.js"></script>
<nomodule>
  <script defer src="bundle.js"></script>
</nomodule>

Alternately a more targeted fix would be a nomodule attribute on <script>:

<script type="module" src="app.js"></script>
<script nomodule defer src="bundle.js"></script>

An alternative involving no new elements or attributes is to use JS-based feature detection, e.g.

<script type="module">
import "app.js";

window.__browserHasModules = true; // SyntaxError will occur otherwise
</script>
<script>
if (!window.__browserHasModules) {
  var s = document.createElement("script");
  s.src = "bundle.js";
  s.defer = true;
  document.head.appendChild(s);
}
</script>

However this will defeat the preload scanner (in the older browser case). You could try to work around it with <link rel="preload"> but then you'd fetch bundle.js even in newer browsers which is sad.

(EDIT: also, the above won't work because module scripts execute at "defer time"; we also would need to defer the evaluation of the above script, perhaps by listening to DOMContentLoaded.)

A third alternative is user-agent sniffing, of course.

Thoughts? Alternate ideas or patterns?

(Note: at first I thought <noscript type="module"> would work but it clearly would not because an old browser which does not support modules would just see it as a normal <noscript> and not execute the script-loading code inside.)

/cc @whatwg/modules

@zcorpan

This comment has been minimized.

Copy link
Member

zcorpan commented Jun 21, 2016

I like a new attribute on script better than a new element.

noscript has weird parsing rules and rendering rules that differ depending on whether scripting is enabled or not. If we add a new element like it, but it doesn't do the weird parsing (i.e. parse as raw text), then people will be confused when e.g. an img still loads inside it.

Do you think we will need a "nomodule2" attribute in the future? If yes, maybe we should make the classic script not run only if the value is the empty string, so we can add nomodule="es2022" or something later?

@annevk

This comment has been minimized.

Copy link
Member

annevk commented Jun 21, 2016

We shouldn't need versioning for modules. I agree that an element seems problematic, both for the reasons Simon states but also because you could not place it inside head.

@domenic

This comment has been minimized.

Copy link
Member

domenic commented Jun 21, 2016

Good points against an element from both of you.

I don't think we'll need nomodule2, but it also seems reasonably harmless to restrict the syntax so that nomodule, nomodule="", and probably nomodule="nomodule" are the only ways of triggering it, just in case. There could conceivably be other cases where we want to disable scripts based on something vaguely related to modules in the future, so reserving that syntactic space would be nice.

For example nomodule="wasm" seems at least possibly-useful if we ever end up adding <script type="wasm">. Although maybe nowasm would be nicer in that case.

@jakearchibald

This comment has been minimized.

Copy link
Collaborator

jakearchibald commented Jan 10, 2017

Although maybe nowasm would be nicer in that case.

Or unlesstypesupported="wasm". No strong feelings though.

@fredck

This comment has been minimized.

Copy link

fredck commented Jan 10, 2017

This looks like optional loading of scripts based on feature detection, exclusively. The fact that we want to check for support for modules is just a case, but it could be anything else at this point.

Therefore, wouldn't it be better to have a generic solution for including/excluding scripts based on any feature? Something like supports="!modules".

@domenic

This comment has been minimized.

Copy link
Member

domenic commented Jan 10, 2017

That kind of generic feature proposal is exactly what I was afraid this would snowball into :(. No, I don't think there's any interest in working on such a general-purpose feature (e.g. specifying what all the features on the platform are).

@fredck

This comment has been minimized.

Copy link

fredck commented Jan 10, 2017

No, I don't think there's any interest in working on such a general-purpose feature (e.g. specifying what all the features on the platform are).

I was sure that was the reason for that, which is indeed understandable.

Couldn't the specs for this be developed in separate phases - first the general feature proposal with an "empty" list of features and then leave it open for proposing features to be included in a "per request" base? I don't think an extensive and comprehensive list is a must have.

But I understand that this may be scary.

@zcorpan

This comment has been minimized.

Copy link
Member

zcorpan commented Jan 10, 2017

APIs to test support for features don't work so well on the Web. hasFeature ended up returning true for any argument, though SVG spec tried to have detailed feature strings. Some of the problems:

  • Dividing things into "features" is difficult. On what granularity do you want to test?
  • Browsers claim support for things they don't support.
  • Browsers claim non-support for things they support.
  • Browsers will have buggy support and claim support or non-support, and maybe some web developers want to use the buggy implementation and others want to avoid it.
  • Doing this well enough to be usable is an opportunity cost; it will take time away from other things like fixing interoperability bugs in the buggy features or adding support for new features.
@jakearchibald

This comment has been minimized.

Copy link
Collaborator

jakearchibald commented Jan 11, 2017

If a browser launches with type="module" support but not nomodule, it'll download & execute both the module and the bundle. The bundle could be modified to exit early if modules are supported, but the download overhead would be a good enough reason for a developer to avoid nomodule until this browser no longer appeared significantly in their stats.

Given that, apologies for bikeshedding. Having nomodule ship with type="module" is way more important than having generic support detection that takes months to work out.

domenic added a commit that referenced this issue Jan 11, 2017

Add <script nomodule> to prevent script evaluation
This allows selective delivery of classic scripts only to older user
agents. Fixes #1442.
@guybedford

This comment has been minimized.

Copy link

guybedford commented Jan 12, 2017

This seems like a useful feature, but I would be incredibly weary of pushing something out too quickly with a sense of urgency, that doesn't actually solve real problems.

To dissuade the sense of urgency, I just put together a production-suitable bootstrap workflow for exactly this sort of thing at https://github.com/guybedford/isomorphic-browser-modules.

The above project uses the same script file as both a module and a script to do its bootstrap, which in theory covers exactly the same need that the nomodule attribute gives - avoiding duplicate fetching.

To really tackle the feature detection problem though, it is about ensuring it is possible to detect practical features that developers would want to provide. I've included my learnings below from the process:

  1. Unfortunately in my example, <script type="module" src="bootstrap.js"><script defer src="bootstrap.js"> results in two separate network requests in Safari, despite the fact that the request headers should be equal, so this approach would still benefit from nomodule.
  2. document.currentScript doesn't seem to be supported for <script type="module"> this is useful for any custom attributes on the module tag that may provide information for fallbacks. Are there plans to support this?
  3. import syntax is going to be absolutely crucial to detect as if found it will cause a syntax error and completely bails out the module load. I would say this is a much more interesting baseline to support for modules workflows, and anything without import should really trigger nomodule for all practical purposes. Short of running eval('import') for a detection which would break CSP, there don't seem to be any easy strategies without some platform work here.
  4. We know there will be more modules features that will be phased in - a resolver, registry API, possible import metadata (import.url etc). These in turn would also need fine-grained feature detections to properly support progressive workflows.

I do hope we can value the fine-grained control of specific feature detection over blunt hammers here. Surely these lessons have been learnt enough times.

@annevk

This comment has been minimized.

Copy link
Member

annevk commented Jan 12, 2017

results in two separate network requests in Safari, despite the fact that the request headers should be equal

That's not a reason to coalesce requests though. There's no standardized coalescing of requests at the moment. In fact, any browser that did would be non-conforming.

document.currentScript

See #1013. currentScript is basically not compatible with shadow trees.

@guybedford

This comment has been minimized.

Copy link

guybedford commented Jan 12, 2017

@annevk thanks for the clarifications. I wasn't aware of that, I just assumed that there would be some coalescing and cache sharing between fetch implementations. Does this mean we will also need an <link rel="preload" as="module" /> for preloading modules? The bootstrap script is only a 400B payload so I don't think having a duplicate request is an inhibiting factor to the workflow and argument though.

@annevk

This comment has been minimized.

Copy link
Member

annevk commented Jan 12, 2017

@guybedford I think you should be able to use <link rel=preload as=script>.

@guybedford

This comment has been minimized.

Copy link

guybedford commented Jan 12, 2017

Apologies as it's off-topic, but I'd hope we could have a more definite story on preload. Unfortunately I can't test this as neither Edge or Safari supports it still. Having across-board preload support for deep module dependencies is really important to allow traditional (non-PUSH) server workflows for modules.

@annevk

This comment has been minimized.

Copy link
Member

annevk commented Jan 12, 2017

I shouldn't have said "I think".

@guybedford

This comment has been minimized.

Copy link

guybedford commented Jan 12, 2017

To distill my argument above, I would suggest clarifying that the nomodule feature correspond to an adequate baseline modules implementation.

This is because nomodule will only be useful for ES workflows if import syntax is supported alongside its implementation. Otherwise we may still get bail out of <script type="module"> on syntax errors for this valid feature. Modular code using features like dynamic import or import.currentScript will be broken on fragmented implementation paths.

Ideally nomodule could clarify that it should only be implemented when alongside implementations of "html modules", "web worker modules", "dynamic import", and syntax support for "import meta forms" (import.x), that simply throw or return undefined on invalid access, but that such throwing can be used for feature detection. In this sense it could act as a stable versioning at some level of module support.

domenic added a commit that referenced this issue Jan 31, 2017

Add <script nomodule> to prevent script evaluation
This allows selective delivery of classic scripts only to older user
agents. Fixes #1442.

alice added a commit to alice/html that referenced this issue Jan 8, 2019

Add <script nomodule> to prevent script evaluation
This allows selective delivery of classic scripts only to older user
agents. Fixes whatwg#1442.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment