Open zcorpan opened 2 years ago
The usage could be something like:
<script type=module async blocking=render>
if (someCondition) {
const { foo } = await import('something.js', {blocking: 'render'});
// do something with foo
}
</script>
I think this is a good idea. Some minor concerns:
This won't solve all use cases, since import()
only works on module scripts. If you have something which is not compatible with being run as a module script (e.g., relies on var
creating global variables, relies on sloppy mode) then you would still need document.write()
.
The JavaScript spec currently does not give us access to the options argument passed to import()
. It should, for cases like this. But passing it through will take some discussion with TC39.
The API shape is unclear. blocking: 'render'
vs. blocking: ['render']
vs. renderBlocking: true
. Note that in the Fetch spec, "render-blocking" is a single boolean flag, so the underlying model is more like renderBlocking: true
. But we have not yet exposed it as an API in fetch()
, so we have flexibility. Whatever we do here sets a strong precedent for fetch()
though, so we should proceed cautiously. /cc @annevk
We have existing non-interop on whether import()
blocks the load event; see https://github.com/whatwg/html/issues/5824. This is only tangentially related but recall that part of the reason we designed the HTML content as blocking="render"
instead of renderblocking=""
is so that we could in the future do things like blocking="loadevent"
. Something to think about.
For classic scripts, you can do this:
<script>
"use strict";
if (someCondition) {
const scriptEl = document.createElement('script');
scriptEl.src = 'something.js';
scriptEl.blocking = 'render';
document.head.append(scriptEl);
}
</script>
Good point about the API shape. Separate booleans for each thing might indeed be better.
I think there's a general issue of how to create a chain of render-blocking resources.
The current HTML spec allowing adding new render-blocking requests only if document.body
hasn't been inserted. So currently, the only way to reliably create a render-blocking chain is to do it in synchronous scripts. For use cases like https://github.com/whatwg/html/issues/7976#issuecomment-1143515463, there's a race condition of whether the script is evaluated first or whether body
is inserted first.
In https://github.com/w3c/csswg-drafts/issues/7271, we have the same issue for font faces, and the current technical choice is to make it block the load
event of the owner style sheet.
For dynamic import()
, does it block the load
event of the owner script?
For dynamic
import()
, does it block theload
event of the owner script?
It does not in the spec. Per https://github.com/whatwg/html/issues/5824 implementations don't quite match the spec, so we might have some flexibility.
But, what is the connection between the load event and render-blocking? I thought the load event would be unrelated.
Sorry I wasn't precise enough. By "making it block the load event", I mean making it a critical subresource, so that it must be loaded (and evaluated if it's an imported module) before we perform the unblock rendering step.
Oh, I see. There is no notion of critical subresource for script elements. We just unblock rendering for them after they run. We don't wait for any fetches they initiate, including those by mechanisms such as fetch()
or import()
or new Image()
.
The current HTML spec allowing adding new render-blocking requests only if
document.body
hasn't been inserted.
I think that should be changed so that new render-blocking requests can be added if there are currently unresolved render-blocking requests, regardless of whether document.body
exists.
I think that should be changed so that new render-blocking requests can be added if there are currently unresolved render-blocking requests, regardless of whether
document.body
exists.
We can do this specially for "secondary" requests (requests as a subresource of some element; not sure if we have a term for that).
We can't do it for HTML elements because it will otherwise widely affect the FCP of current pages.
OK, I can see that render-blocking declarative markup should be in head
. But for async scripts (that are themselves render-blocking) that call fetch()
or import()
or create a new classic script
, it seems OK? script
elements have a "parser-inserted" flag, so they could check document.body
only if that flag is set.
As far as the TC39 side: I'm happy to help thread this through there if changes are needed. We already have import assertions, and this could be another category of key/value pairs analogous to that. With what's slated for JS, I believe it would already be possible for HTML to support syntax like import("something.js", { assert: { blocking: "render" } })
, though arguably this would be a bit of an abuse of the concept of "assert".
In this Twitter thread about obsoleting
document.write()
(#7977) @matthewp pointed out that it's the only way to include conditional script dependencies that block the first paint. @domenic said thatimport()
creates a new module graph and so will not block rendering even if the<script>
hadblocking=render
.Possible ways to address this:
blocking=render
toimport()
e.g. as an options bag in a second argumentimport('something.js', {blocking: 'render'})
. Issue: how to check for support?blocking=render
to<link rel=modulepreload>
and specify that it should block rendering until the corresponding module is fully loaded and executed, not just fetched. (See #7896.) Less ergonomic than the above for the conditional dependency use case, though.cc @xiaochengh @whatwg/modules