whatwg / html

HTML Standard
https://html.spec.whatwg.org/multipage/
Other
8.16k stars 2.69k forks source link

Consider adding `blocking=render` for dynamic `import()` #7976

Open zcorpan opened 2 years ago

zcorpan commented 2 years ago

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 that import() creates a new module graph and so will not block rendering even if the <script> had blocking=render.

Possible ways to address this:

cc @xiaochengh @whatwg/modules

zcorpan commented 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>
domenic commented 2 years ago

I think this is a good idea. Some minor concerns:

zcorpan commented 2 years ago

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.

xiaochengh commented 2 years ago

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?

domenic commented 2 years ago

For dynamic import(), does it block the load 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.

xiaochengh commented 2 years ago

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.

domenic commented 2 years ago

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().

zcorpan commented 2 years ago

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.

xiaochengh commented 2 years ago

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.

zcorpan commented 2 years ago

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.

littledan commented 2 years ago

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".