Skip to content
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

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

Open
zcorpan opened this issue Jun 1, 2022 · 11 comments
Open

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

zcorpan opened this issue Jun 1, 2022 · 11 comments
Labels
addition/proposal New features or enhancements

Comments

@zcorpan
Copy link
Member

zcorpan commented Jun 1, 2022

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:

  • Add blocking=render to import() e.g. as an options bag in a second argument import('something.js', {blocking: 'render'}). Issue: how to check for support?
  • Add back 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 Behavior of blocking attribute on a preload link #7896.) Less ergonomic than the above for the conditional dependency use case, though.

cc @xiaochengh @whatwg/modules

@zcorpan zcorpan added the addition/proposal New features or enhancements label Jun 1, 2022
@zcorpan
Copy link
Member Author

zcorpan commented Jun 1, 2022

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
Copy link
Member

domenic commented Jun 1, 2022

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 Should dynamic import()ed script loading delay Window load events? #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.

@zcorpan
Copy link
Member Author

zcorpan commented Jun 1, 2022

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
Copy link
Contributor

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 #7976 (comment), there's a race condition of whether the script is evaluated first or whether body is inserted first.

In w3c/csswg-drafts#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
Copy link
Member

domenic commented Jun 1, 2022

For dynamic import(), does it block the load event of the owner script?

It does not in the spec. Per #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
Copy link
Contributor

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
Copy link
Member

domenic commented Jun 1, 2022

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
Copy link
Member Author

zcorpan commented Jun 2, 2022

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
Copy link
Contributor

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
Copy link
Member Author

zcorpan commented Jun 7, 2022

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
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements
Development

No branches or pull requests

4 participants