whatwg / html

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

Module script dependencies and fetch priority #10276

Open yoavweiss opened 5 months ago

yoavweiss commented 5 months ago

What is the issue with the HTML Standard?

According to descendant script fetch options, module script dependencies are fetched with an "auto" priority. I'd love to understand the reasoning for this - given that the descendants are blocking the parent from executing, if the parent got its priority upgraded, shouldn't the same apply to these blocking dependencies? In the inverse case, if the parent got its priority lowered, the dependencies are similarly of lower priority.

Is there a reason the descendants don't simply inherit the fetch priority from the parent?

^^ @domfarolino

domfarolino commented 4 months ago

See https://github.com/whatwg/html/pull/8470#pullrequestreview-1274139279 and https://github.com/whatwg/html/pull/8470#issuecomment-1411263009. @pmeenan mentioned that in the spec discussion (presumably this means discussion in the Priority Hints spec, before the HTML upstreaming was started) that priority would not cascade down / be inherited by dependent resources. The why behind this is not obvious to me, so maybe Pat can link to some of the spec discussion he referred to and help us clarify this?

pmeenan commented 4 months ago

It was a simplifying decision around scripts, iframes and modules that the fetchpriority was specifically just for the resource that it was tagged on and that there were no downstream effects on fetching or execution.

At least in Chrome, "auto" for a module script dependency should already be high and you wouldn't be able to boost it anymore. It's less clear that you'd want dependencies to be lowered again after the main resource was loaded or what that would even mean (i.e. for a script, would every fetch from within the script get a forced priority change, what would break if that were the case).

It's a bit clearer in the iFrame case for something like a video embed or chat widget (ignore for now that fetchpriority doesn't actually apply to iframes but consider the grouping). Just because you want the frame itself to load later doesn't mean you want it to load slowly once it has started.

So, we decided that fetchpriority on the main resource would control when things started but would then get out of the way and if we had a case for more fine-grained control over grouped execution contexts that that would be something else solved at a different time (once the use case for it was clear).

yoavweiss commented 4 months ago

At least in Chrome, "auto" for a module script dependency should already be high and you wouldn't be able to boost it anymore. It's less clear that you'd want dependencies to be lowered again after the main resource was loaded or what that would even mean (i.e. for a script, would every fetch from within the script get a forced priority change, what would break if that were the case).

I think there's a difference here between "script A loads script B" (in which I agree we don't want to inherit priority automatically) and "script A statically imports script B" in which script A will not execute until script B is there. I think it makes sense to consider A & B the same resource in this case, as they are totally co-dependent.

Aside: I missed the fact that module scripts don't get priority modifications in Chromium. That means that this doesn't matter in practice (in Chromium) when upgrading priority, but can definitely matter when downgrading it.

pmeenan commented 4 months ago

Even in the downgrade case, because of the late discover of imports, it's not obvious that cascading the downgrade is the "right" thing to do.

Example

Let's say we have a page with a module script a.js that imports b.js and there are 100 images on the page where the first one is a hero image:

...
<script type="module" src="a.js"></script>
<img src="1.jpg">
<img src="2.jpg">
...

On the browser side, some simplifying assumptions for the purpose of the example (close enough to actual behavior):

Also assume we have a browser and server that honor prioritization:

Let's say that a.js is a script that is important to the page but not to the user experience, like an analytics script where you want it to run as soon as possible so you can track abandons but not block the user experience.

Default loading behavior

By default, the order for the resources being loaded would be something like:

  1. a.js is transferred
  2. 1.jpg is transferred (already in-flight when b.js is being requested)
  3. b.js is transferred
  4. a.js executes
  5. Images 2-100 are transferred

Adjusting with fetchpriority

By default, a.js is delaying the hero image so we boost the priority of the hero image and lower the priority of a.js:

...
<script type="module" src="a.js" fetchpriority=low></script>
<img src="1.jpg" fetchpriority=high>
<img src="2.jpg">
...

Priority does NOT cascade

In the case where the priority for module scripts does not affect the import priorities, b.js will be transferred at a high priority once it is discovered after a.js has loaded:

  1. 1.jpg is transferred
  2. a.js is transferred
  3. 2.jpg is transferred (already in-flight)
  4. b.js is transferred
  5. a.js executes
  6. Images 3-100 are transferred

It's not perfect, but it gets a.js out of the way of the hero image but still allows it to execute before the other images load.

Priority cascades

In the case where the priority for module scripts does affect the import priorities, b.js will be transferred at a low priority once it is discovered after a.js has loaded. This is after all of the images have already been discovered so it will be queued behind all of them:

  1. 1.jpg is transferred
  2. a.js is transferred
  3. Images 2-100 are transferred
  4. b.js is transferred
  5. a.js executes

This delays the execution of a.js until after everything else on the page has already loaded. That's a pretty big footgun if it wasn't expected.

Bundles

In the case of bundling the modules at build time, b.js would be included in a.js and it would effectively load at the same time as a.js:

  1. 1.jpg is transferred
  2. a.js is transferred (with b.js included)
  3. a.js executes
  4. Images 3-100 are transferred

Thoughts

Not cascading is the closest to "bundled" behavior and basically mimics the behavior of Fonts where late-discovery of resources that are needed by the thing they are loaded by are loaded at a high priority because they are needed now.

The discovery changes a bit when it comes to import maps or preloads so I could see making a case for "fetchpriority=low on a module script where the imports are already in-flight does not modify the priority of the existing requests" so you could get the ordering directly from the import maps or preloads but I don't think it should be the default behavior.

The other option would be to extend loading=lazy or something like that to module scripts of you want a situation where the imports all load at idle time.