Open noamr opened 2 years ago
+1 for the suggestion. I'd like to hear @hiroshige-g's opinion.
Note that Chromium's current implementation is somewhat different from the ideal. As the design document explains it makes a request upon a reception of an Early Hints preload and put the response in the HTTP cache (no network request happens when there is a fresh response in the cache already). When the document makes a request for the same resource the response hopefully comes from the HTTP cache. It doesn't recognize the semantics of modulepreload
now.
We don't think the current approach is a long-term solution and we would like to have a proper implementation in the future -- doing so requires a lot of engineering work and we are trying to figure out a reasonable approach.
cc: @yutakahirano
I suggest the following strategy in the spec (and implementation):
- Keep the existing Chromium behavior when the early hint headers are processed
- Once the document is created, also preload the script graph immediately as if the document had a regular
modulepreload
header.
Just to clarify, does this mean the following?
modulepreload
headers are processed, preload the scripts as if they were preload
.Note that preload the script graph anyway doesn't fetch the dependencies (at least in Chromium). Step 3 is marked as "optionally perform".
I suggest the following strategy in the spec (and implementation):
- Keep the existing Chromium behavior when the early hint headers are processed
- Once the document is created, also preload the script graph immediately as if the document had a regular
modulepreload
header.Just to clarify, does this mean the following?
- When the early hint
modulepreload
headers are processed, preload the scripts as if they werepreload
.
Right, with the "special" aspects of modulepreload
such as treating empty as
as script
.
- Once the document is created, trigger preload the script graph.
Exactly, passing in the response we already have and modifying that algorithm to optionally accept a response argument.
Note that preload the script graph anyway doesn't fetch the dependencies (at least in Chromium). Step 3 is marked as "optionally perform".
Right, but at the very least the module is added to the module map, and this "optionally perform" would become viable for early hints.
Thanks for clarification!
After looking further at the Chromium Early Hints source code, "preload the scripts as if they were preload
" is actually a tweaked version for module scripts (and thus there are no exactly corresponding case in <link rel=preload>
).
cc @domenic @irori.
Thanks for clarification!
After looking further at the Chromium Early Hints source code, "preload the scripts as if they were
preload
" is actually a tweaked version for module scripts (and thus there are no exactly corresponding case in<link rel=preload>
).cc @domenic @irori.
Yes, that's what I meant by "special" aspects of modulepreload
.
Anyway, I can work on a PR along those lines, where early hints performs the modulepreload
steps all the way until the response, and continues to fetch the script graph with that response once the document is initialized. Perhaps a PR would be easier to discuss.
In a project I'm working on, I made the choice to adopt ES6+ syntax and ESM native modules, with an "importmap" to faccilitate versioning/cache busting. Upon transitioning the preload strategy, I believe I am now stuck with no options available that aren't actively harmful in some way. The reason the project has a preload strategy, is that some endpoints involve non-trivial server-side logic where Page Load Time benefits from concurrently requesting static assets (flush headers before the HTTP body. HTTP body is cannot feasibly be flushed in parts).
Let me back up a bit, and share what my assumptions are at this point. Correct me if any of this is wrong!
rel=preload
supports scripts but not modules. Modules are requested with different CORS default settings than scripts, and so the eventual real request for <script type=module src=…>
would not match the preloaded fetch for rel=preload;as=script
. When faced with a proposal to add inconsistent CORS behaviour to rel=preload, it was decided (understandably) to favour creating a separate rel=modulepreload
keyword instead. — https://github.com/w3c/preload/issues/136
rel=modulepreload
works and even has cross-browser support for importmaps and eager dependency resolution. But, the standard does not specify "Has Link processing" for this, thus is currently limited to HTML <link>
tags, and not available to Early Hints / HTTP Link headers.
– https://html.spec.whatwg.org/multipage/links.html#linkTypes:link-type-preload
I considered using rel=preload;as=script;crossorigin
as a way to take responsibility for CORS on my own and help the browser. This works in Chrome, and thus I can use this today to preload main.js?v1
. I can also optionally preload the flattened list in its entirety and add foo.js?v1
and bar.js?v1
too. In this way, I would consider rel=preload to be fulfilling the need of an a low-level extendable primitive, in the spirit of The Extensible Web Manifesto.
Unfortunately, Firefox differs in how it makes or matches requests and ends up making a duplicate request. Is this a bug in Chrome for cache over-use, or Firefox bug for cache under-use?
rel=preload;as=script;type=script
was briefly supported by Firefox, including in Link headers, and appears to have worked correctly for this purpose, the same way as the above still does in Chrome today. But, Mozilla removed this feature after rel=modulepreload
was standardised.
– https://bugzilla.mozilla.org/show_bug.cgi?id=1803744
Contrary to the spec, Firefox already allows rel=modulepreload
to be used in HTTP Link headers. This was implemented and shipped last year in Firefox 116 (https://bugzilla.mozilla.org/show_bug.cgi?id=1773056#c4, https://bugzilla.mozilla.org/show_bug.cgi?id=1798319, https://hg.mozilla.org/mozilla-central/rev/5cafcb0a03c8).
There is also a spec proposal by @noamr at https://github.com/whatwg/html/pull/7862 and https://github.com/whatwg/html/issues/7854.
Unfortunately, this is too powerful for its own good. When given Link: </static/main.js?v1>;rel=modulepreload
, Firefox 123 (latest stable as of writing) correctly preloads main.js?v1
, but then also ends up making unused duplicate requests for (unversioned) foo.js
and bar.js
. This can't be mitigated at the moment, since recursion is on by default (not configurable/extensible) and importmaps support is still pending standardisation at https://github.com/whatwg/html/issues/9274.
It seeems thus that today, when transitioning from having 3 classic scripts to 3 ESM files in a project, there is currently no option available that provides the same level of correct/complete preloading as was already available to classic scripts.
What I tried:
rel=preload;as=script;crossorigin
.
preload either one or all scripts with rel=modulepreload
.
Given
Link: </static/bar.js?dev>;rel=modulepreload,</static/foo.js?dev>;rel=modulepreload,</static/main.js?dev>;rel=modulepreload
<script type="importmap">
{
"imports": {
"/static/foo.js": "/static/foo.js?dev",
"/static/bar.js": "/static/bar.js?dev",
"/static/main.js": "/static/main.js?dev"
}
}
</script>
<link rel="modulepreload" href="/static/foo.js?dev">
<link rel="modulepreload" href="/static/bar.js?dev">
<script type="module" src="/static/main.js?dev"></script>
Safari is making 9 requests, 3x for each of the 3 files. All with the correct URL (no bare ones, WebKit Inspector hides querystring) and CORS setting. It even fetches main?v1
three times despite only being requested twice (Link header and script tag). The middle one corresponds to a line an unrelated element after the importmap script. That seems to trigger Safari into making a request somehow. Notice it is doing the same for stylesheets as well. It is requesting style.css
three times. Once for the Link header. Once for no apparent reason after reading an importmap relating to JS files. And a third time for the actual stylesheet link tag. And CSS isn't even at issue in terms of potential preload mismatch. I'm going to assume I've hit an edge case and that under some definition of normal circumstances, simple CSS preloads aren't broken in this way in Safari.
main?dev
preload has been consumed by the DOM, and the definitive importmap is known, utilized, and satisfied; Chrome then starts to revisit the old modulepreload response it got earlier and decides to make two additional duplicate requests for the 2 unversioned imports. So... in conclusion, it appears there are no options available to progressively enhance performance by preloading some (or all) ESM files as module scripts. Each of the options I found is actively harmful in at least one major browser. I'm leaving this here in case I've missed something. I'd love to know of a way that I can at least declare one of the JS resources for preloading, in a way that works in 1 browser and is safely ignored in any browsers that don't support it yet.
@zqianem
@Krinkle, apologies for the delay; my understanding of the modulepreload side of the problem is this:
Early Hints modulepreload and import maps can't work together because conceptually, Early Hints prepends a <link rel="modulepreload">
to the document's head^1, which since it comes before the <script type="importmap">
, should disable the import map according to spec^2.
However, Chromium seems to have a bug^3 where Early Hint modulepreloads don't disable the import map, (but having an actual <link rel="modulepreload">
element in the HTML will); this may have led to the belief that the above combination was feasible when it really wasn't.
Here's a minimal repro of the current situation: https://github.com/zqianem/modulepreload-krinkle-repro
For both Firefox and Chromium, the recursion from the Early Hints only starts after index.html
is parsed, even when index.html
doesn't have any content.
Currently in the spec, early hints work only for
preload
(and soon forpreconnect
). In Chromium source code (as I understand it) and in WPT,modulepreload
is also supported, but it's treated like apreload
in the sense that it doesn't start loading module dependencies. It still uses the special fetching defaults/parameters ofmodulepreload
like havingscript
as the default foras
.I see how it's problematic to start creating actual script objects before the document is initialized, but it also changes the semantics of
modulepreload
when it's an early hint, which might be confusing - e.g. if the developer doesn't add an actual equivalent<link rel=modulepreload>
to the document, the dependencies will only be fetched when the module is imported in practice.I suggest the following strategy in the spec (and implementation):
modulepreload
header.