← All issues

Dynamic `import.defer()` semantics

7f06296

JSTests/modules/import-defer-dynamic-evaluation.js

+const ns = await import.defer("./import-defer/eval-tracker.js");
+shouldBe(JSON.stringify(globalThis.deferEvaluations), "[]");
+
+// A symbol-keyed lookup must not trigger evaluation either.
+shouldBe(ns[Symbol.toStringTag], "Deferred Module");
+shouldBe(JSON.stringify(globalThis.deferEvaluations), "[]");
+
+// First non-symbol access evaluates the module synchronously.
+shouldBe(ns.value, 42);
+shouldBe(JSON.stringify(globalThis.deferEvaluations), JSON.stringify(["eval-tracker"]));
+
+// Subsequent import.defer() calls hand back the same namespace and never re-evaluate.
+const ns2 = await import.defer("./import-defer/eval-tracker.js");
+shouldBe(ns2, ns);

The TC39 Deferred Module Evaluation proposal separates module linking from module execution: a deferred namespace proxy is returned immediately and the module body only runs on first non-Symbol property access. JSC's module pipeline is split across the engine (graph traversal, bytecode, microtask scheduling) and WebCore bindings (ScriptModuleLoader, JSDOMGlobalObject), so the new "import phase" parameter must be propagated through both layers.

This commit implements the dynamic import.defer(specifier) form. The module graph is loaded and linked but the deferred root is not executed; GatherAsynchronousTransitiveDependencies() collects unexecuted top-level-await (TLA) modules in post-order, evaluates them through two new internal microtasks (DynamicImportDeferLoadSettled, DynamicImportDeferDependencySettled), and resolves the returned promise to a deferred module namespace once all settle. The AND-join that waits for all TLA dep promises reuses JSPromiseCombinatorsGlobalContext as a shared counter cell, a pattern borrowed from Promise.all internals but with the critical spec constraint that then must never be looked up on the dependency promises.

import.defer(specifier)
  ├─► load() + link()
  ├─► GatherAsyncTransitiveDeps() → [dep1, dep2, ...]
  ├─► evaluate(depN) ──► AND-join counter (DynamicImportDeferDependencySettled)
  └─► resolve(deferredNamespace)

Namespace proxy dispatch:
  Symbol key  ──► return value  (NO evaluation)
  String key  ──► evaluate() ──► return value

This change introduces new JS-visible API, new microtask types, a new promise AND-join, and new namespace proxy dispatch semantics spanning every layer of the JSC module system — historically the highest-density area for security bugs in JS engines.

🔒

New promise AND-join mechanics, namespace proxy dispatch boundaries, and cross-layer import phase propagation all have audit-worthy angles.

Subscribe to read more