Script-injected import map makes Firefox fetch every module twice and the page never fires `load` — tab spinner and "Transferring data from…" forever
Categories
(Core :: DOM: Core & HTML, defect)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox155 | --- | fixed |
People
(Reporter: martin.ankerl, Assigned: allstars.chh)
References
Details
Attachments
(2 files)
User Agent: Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0
Steps to reproduce:
Attached: firefox-importmap-hang-repro.tar.gz (self-contained, no build step).
- Extract, then
python3 -m http.server 8000inside the directory. - Open http://localhost:8000/ (=
index.html, a reduced page) in Firefox.
It hangs on most loads; reload 2–3 times if the first one completes.
full.htmlis the unreduced original page and hangs on virtually every load. control-static.htmlis byte-identical except that the import map is a
literal<script type="importmap">instead of one built and injected by an
inline script — it never hangs.
The page shape that triggers it, in essence:
<link rel="icon" href="assets/img/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="assets/css/app.css"> <!-- parser-blocking -->
<script>
// builds {"./x.js": "./x.js?v=67", ...} for ~30 modules and injects it
var s = document.createElement("script");
s.type = "importmap";
s.textContent = JSON.stringify({ imports: imports });
document.currentScript.after(s);
</script>
...
<script type="module" src="assets/js/map.js?v=67"></script> <!-- end of body -->
The module graph is ~10 modules deep/wide; the CSS declares three @font-face
woff2 fonts that the page uses; the entry module dynamically imports a
dictionary module and then inserts two <img src="....svg"> elements.
Actual results:
document.readyStatestays"interactive"forever; theloadevent never
fires (verified via Marionette polling for 25s+, andfirefox --screenshot
on such a page never writes its file — observed still waiting after 12 min).- Tab throbber spins forever; status panel permanently shows
"Transferring data from localhost…". - DevTools' network panel shows every request completed (all 200), and the
server log confirms every file was served and every connection closed. - Additionally, every nested module is fetched TWICE: once unversioned —
i.e. resolved as if the import map did not exist — and once through the map
(?v=67). Visible in the server log on every affected load.
MOZ_LOG="LoadGroup:5,DocLoader:5" on a hanging load shows requests that are
added to the document's load group and never removed (everything else gets a
matching Removing line):
LOADGROUP [7f...]: Adding request 7f... http://localhost:8002/v2/assets/img/favicon.svg (count=11).
LOADGROUP [7f...]: Firing OnStartRequest for request 7f... (foreground count=11).
... no Removing/OnStopRequest for this request ever appears ...
The leaked entries vary per load — the SVG favicon, the browser's fallback
/favicon.ico (a 404!) when no icon link is present, and/or script-inserted
<img> elements — always image-ish loads that start while the load group is
busy. about:document-onload-blocker consequently never clears, which is why
load never fires. The corresponding HTTP channels DO complete (the stuck
<img>s render fine, devtools shows the transfers finished); it is the
load-group bookkeeping entry that leaks.
Expected results:
- The import map, injected synchronously before the parser reaches the module
script, should either apply to the whole graph (one fetch per module) or be
rejected — not produce two full fetch passes. - The load event should fire once all loads complete; no load-group entry
should outlive its finished channel.
Notes / analysis
- Suspected first half: the parser-blocking stylesheet delays the inline
script that injects the map, while the module entry (preloaded
speculatively) starts its dependency fetches with no import map in effect —
those unversioned fetches later race their re-resolved?v=Ntwins.
Consistent with this, the double fetch disappears if the stylesheet is
removed or the import map is written statically. - The never-finishing load reproduces with
network.http.tailing.enabled=false,
withnetwork.http.throttle.enableat its default (false), over both
HTTP/1.0 (python http.server default) and HTTP/1.1, with
ui.prefersReducedMotion=1, and on a real HTTPS/H2 deployment (GitHub
Pages) — so it is not specific to the local server. - Fresh profile every run; no extensions.
- On the affected site, switching the pages back to a static
<script type="importmap">(content otherwise identical) fixed both
symptoms immediately.
Comment 1•1 month ago
|
||
The Bugbug bot thinks this bug should belong to the 'Core::DOM: Networking' component, and is moving the bug to that component. Please correct in case you think the bot is wrong.
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
Comment 2•1 month ago
|
||
Moving to DOM: Core & HTML, since this looks like a DOM issue rather than Networking.
The double fetch comes from the HTML preload scanner speculatively resolving/fetching the module graph before the injected <script type="importmap"> is registered. The hang appears to be a module-loading onload-block leak: an orphaned preloaded ScriptLoadContext calls Document::BlockOnload() and is never unblocked, so mOnloadBlockCount never returns to 0 and the load event never fires.
The fix likely belongs in dom/script, around the stale module-preload adopt/discard path when an import map is registered late.
Updated•1 month ago
|
Comment 3•26 days ago
|
||
Hi Yoshi, can you please help triage this and suggest the severity? Thank you!
| Assignee | ||
Updated•22 days ago
|
| Assignee | ||
Comment 4•14 days ago
|
||
A off-thread compiled ScriptLoadRequest will block the onload handler of the
document until it's done. But if it's canceled (for example, an import map is
inserted), it should also unblock the onload handler.
Add a blocker_2054295.sjs to do the following:
?hold-parser - to hold off the parser, so the preloader can preload the modules.
?block-dep - to block the fetching of the module dependencies to simulate the
async fetching behavior.
?release-dep - return the result of fetching dependencies.
Timeline:
- ?hold-parser - Hold off the parser.
- preload service preloads "bug_2054295_entry.mjs"
- "bug_2054295_entry.mjs" starts fetching dependencies
- unblock to parser
- hold off the fetching of dependencies
- parser continues, found injection of the import map tag.
- Register ImportMap -> cancel preloadingf requests
- ?release-dep to return the fetching of the depenedencies
- Check onload handler is called.
Updated•23 hours ago
|
Description
•