Bug 324459

Summary: Module worker's top-level script is evaluated again when the worker imports its own URL
Product: WebKit Reporter: mail.tomlimb
Component: JavaScriptCoreAssignee: Nobody <webkit-unassigned>
Status: NEW    
Severity: Normal CC: ap
Priority: P2    
Version: Safari 26   
Hardware: Mac (Apple Silicon)   
OS: macOS 26   
Attachments:
Description Flags
Repro needs a local web server, because module workers don't load from a file:// address. Running python3 -m http.server in the unzipped folder, then opening http://localhost:8000, is enough. none

mail.tomlimb
Reported 2026-09-17 10:41:12 PDT
Created attachment 481409 [details] Repro needs a local web server, because module workers don't load from a file:// address. Running python3 -m http.server in the unzipped folder, then opening http://localhost:8000, is enough. In a module worker (new Worker(url, { type: "module" })), any import of the worker's own top-level script URL creates a second module instance and evaluates the script again. This happens with a static import cycle, with import(), and with a dynamically imported chunk that statically imports the entry. Chrome 153 and Firefox 156 return the existing module. The same pattern in a document (not a worker) is fine in Safari. Per the HTML Standard, the worker's top-level script is fetched through the WorkerGlobalScope's module map, keyed by URL, and "the module map is used to ensure that imported module scripts are only fetched, parsed, and evaluated once per Document or worker" (https://html.spec.whatwg.org/multipage/webappapis.html#module-map). Safari behaves as if the worker's top-level script isn't in its module map. Real-world impact: bundlers (Rollup, Vite) often place shared code in a worker's entry chunk, and lazily loaded chunks import it back. In Safari this silently duplicates module-level state (registries, singletons) and re-runs top-level side effects. Steps to reproduce Serve the three files below from any local web server. Open index.html in Safari. Expected: {"evals":1,"same":true}, as in Chrome and Firefox. Actual: {"evals":2,"same":false}. Versions: Safari 26.5 (macOS 26.5.1). Also reported in Safari 26.3: https://github.com/brandon-fryslie/slopspot-paste/pull/134 index.html <!doctype html> <pre id="out">running</pre> <script> const w = new Worker(new URL("./w.js", location.href), { type: "module" }); w.onmessage = (e) => (document.getElementById("out").textContent = JSON.stringify(e.data)); w.onerror = (e) => (document.getElementById("out").textContent = "error: " + e.message); setTimeout(() => w.postMessage("go"), 100); </script> w.js import { helperSeesId } from "./helper.js"; export const id = Math.random(); globalThis.evals = (globalThis.evals ?? 0) + 1; self.onmessage = () => self.postMessage({ evals: globalThis.evals, same: helperSeesId() === id }); helper.js import * as entry from "./w.js"; export const helperSeesId = () => entry.id; Also affected (same result): await import("./w.js") from inside w.js; a chunk loaded with import() that does import { x } from "./w.js"; importing the entry by absolute URL. Not affected: the same module graph on a page rather than in a worker; worker entry and chunk both importing a separate third module. No web-platform test appears to cover a worker importing its own top-level module; one would be worth adding under workers/modules/.
Attachments
Repro needs a local web server, because module workers don't load from a file:// address. Running python3 -m http.server in the unzipped folder, then opening http://localhost:8000, is enough. (990 bytes, application/zip)
2026-09-17 10:41 PDT, mail.tomlimb
no flags
mail.tomlimb
Comment 1 2026-09-17 11:14:25 PDT
Full context in dev.to : dev.to/tom_limb/safari-runs-a-module-workers-entry-file-twice-if-anything-imports-it-dbp
Alexey Proskuryakov
Comment 2 2026-09-17 15:32:03 PDT
Thank you for the report! Could you please try Safari 27? As described in https://webkit.org/blog/18227/fixing-top-level-await-in-safari/, the loader was rewritten for the latest release.
Note You need to log in before you can comment on or make changes to this bug.