Skip to content

perf(ext): convert ext/fetch JS sources to lazy-loaded scripts#33784

Merged
bartlomieju merged 4 commits into
mainfrom
perf/lazy-loaded-fetch
May 3, 2026
Merged

perf(ext): convert ext/fetch JS sources to lazy-loaded scripts#33784
bartlomieju merged 4 commits into
mainfrom
perf/lazy-loaded-fetch

Conversation

@bartlomieju

Copy link
Copy Markdown
Member

Summary

Follow-up to #33760 and #33779. Converts all 8 ESM JS sources in ext/fetch to IIFE-wrapped lazy-loaded scripts (lazy_loaded_js).

  • 20_headers.js, 21_formdata.js, 22_body.js, 22_http_client.js, 23_request.js, 23_response.js, 26_fetch.js, 27_eventsource.js
  • 26_fetch.js imports from ext/telemetry (TypeScript ESM, can't be converted yet) -- bridged via internals.__telemetry / internals.__telemetryUtil set in the telemetry source files
  • All other cross-extension deps use core.loadExtScript() (ext/web, ext/webidl, ext/net are already lazy_loaded_js)

This also eliminates the internals.__fetchRequest/Response/Headers bridges from #33778 since ext/cache can now use core.loadExtScript() for ext/fetch directly.

Test plan

  • cargo build --bin deno succeeds
  • Smoke tested: fetch, Request, Response, Headers, EventSource all work
  • CI

…ernals bridge

These are mutable variables set during bootstrap(), after the bridge
object is created. A plain property captures the initial value (false/[]),
so use getters to always read the current value.

@fibibot fibibot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Substance follows the established lazy_loaded_js pattern from #33760/#33778/#33779/#33780 — IIFE wrap, globalThis.__bootstrap (or in this case internals for cross-extension symbol bridges), core.loadExtScript() for already-converted extensions, return {...} instead of export {...}. The 8 ext/fetch JS files migrate cleanly to the new shape.

Real regression in OTEL: ESM live-binding semantics lost in destructuring

All 10 test specs debug shards across all 4 platforms fail on specs::cli::otel_basic::basic_grpc with the same diff:

Expected:  "kind": 3,    (SPAN_KIND_CLIENT — outgoing fetch)
Actual:    "kind": 2,    (SPAN_KIND_SERVER — wrong)

The root cause is the telemetry bridge in ext/fetch/26_fetch.js:347–360:

const {
  builtinTracer,
  ContextManager,
  enterSpan,
  PROPAGATORS,
  restoreSnapshot,
  TRACING_ENABLED,
} = internals.__telemetry;

Destructured from internals.__telemetry, which ext/telemetry/telemetry.ts:1915–1925 sets up as:

internals.__telemetry = {
  builtinTracer,
  ContextManager,
  enterSpan,
  get PROPAGATORS() { return PROPAGATORS; },     // live getter
  restoreSnapshot,
  get TRACING_ENABLED() { return TRACING_ENABLED; }, // live getter
};

The get accessors were added correctly because PROPAGATORS and TRACING_ENABLED are mutable module bindings — they're populated later at bootstrap (when OTEL is enabled via flags / env vars). ESM import would have given the consumer a live binding that always reflects the current value. But destructuring const { PROPAGATORS, TRACING_ENABLED } = internals.__telemetry; invokes the getters exactly once at destructuring time, then captures the returned value — which is the pre-bootstrap default (undefined or the initial value).

Downstream effects:

  • TRACING_ENABLED evaluates as falsy in the fetch instrumentation path → the wrong code branch fires → the span gets the wrong kind → test fails with 2 !== 3.

Fix shape

Two options that preserve the live-binding semantic:

  1. Don't destructure, access through the live object on each use:

    const telemetry = internals.__telemetry;
    ...
    if (!telemetry.TRACING_ENABLED) return;
    const tracer = telemetry.builtinTracer();

    Each property access goes through the getter, so reads always see the current value. The downside is more verbose call sites.

  2. Convert PROPAGATORS and TRACING_ENABLED from let bindings to functions (isTracingEnabled(), getPropagators()). The bridge then exposes plain function references that closure over the mutable state. Destructuring is safe again and the call sites stay readable.

Option 2 is the cleaner long-term shape since it makes the live-binding requirement explicit at the API. Option 1 is the smaller diff for unblocking this PR.

The other already-bridged getters (__telemetry.builtinTracer, enterSpan, restoreSnapshot, ContextManager, updateSpanFromClientResponse, etc.) are function references and work fine through destructuring — only the two non-function let bindings have this pitfall.

Other notes

  • The body's claim that this also drops the internals.__fetchRequest/Response/Headers bridges from #33778 is right and verifiable from the diff — ext/cache/01_cache.js now uses core.loadExtScript() directly. Net cleanup.
  • 100/124 success at this head, 13 in flight, 10 failures (all the same OTEL kind mismatch).

Destructuring from the internals bridge object captures the value at
load time, losing the live binding that ESM imports provide. Access
through the object reference so the getter is called each time.
@bartlomieju
bartlomieju enabled auto-merge (squash) May 3, 2026 08:08
@bartlomieju
bartlomieju merged commit c532c63 into main May 3, 2026
136 checks passed
@bartlomieju
bartlomieju deleted the perf/lazy-loaded-fetch branch May 3, 2026 08:19
bartlomieju added a commit that referenced this pull request May 3, 2026
Follow-up to #33784. Converts `ext/fs/30_fs.js` from ESM to
IIFE-wrapped lazy-loaded script (`lazy_loaded_js`), eliminating ESM
module resolution overhead.

Updates three consumers to use `core.loadExtScript()`:
- `ext/process/40_process.js` (`FsFile` import)
- `ext/node/polyfills/_process/process.ts` (`fs` namespace import)
- `runtime/js/90_deno_ns.js` (`fs` namespace import)

No cross-extension blockers -- `30_fs.js` already uses
`core.loadExtScript()` for its `ext:deno_io` and `ext:deno_web`
dependencies (converted in #33798).
bartlomieju added a commit that referenced this pull request May 4, 2026
… scripts (#33817)

- Convert `ext/process/40_process.js` from ESM to IIFE-wrapped
lazy-loaded script (`lazy_loaded_js`)
- Convert `ext/http/00_serve.ts`, `ext/http/01_http.js`,
`ext/http/02_websocket.ts` from ESM to IIFE-wrapped lazy-loaded scripts
- Update all consumers (`runtime/js/90_deno_ns.js`,
`runtime/js/99_main.js`, `ext/node/polyfills/child_process.ts`,
`ext/node/polyfills/internal/child_process.ts`) to use
`core.loadExtScript()` instead of ESM imports
- Replace `import.meta.log` calls with `internals.log` in `00_serve.ts`

Follows the same pattern established in #33778, #33780, #33784, #33800,
and #33801.
bartlomieju added a commit that referenced this pull request May 4, 2026
…ripts (#33818)

## Summary

- Convert `ext/kv/01_db.ts` from ESM to IIFE-wrapped lazy-loaded script
(`lazy_loaded_js`)
- Convert `ext/webgpu/00_init.js` from ESM to IIFE-wrapped lazy-loaded
script (`lazy_loaded_js`)
- Update all consumers (`runtime/js/90_deno_ns.js`,
`runtime/js/98_global_scope_shared.js`,
`runtime/js/98_global_scope_window.js`,
`runtime/js/98_global_scope_worker.js`) to use `core.loadExtScript()`
instead of ESM imports
- Replace `import.meta.log` call with `internals.log` in `01_db.ts`

Follows the same pattern established in #33778, #33780, #33784, #33800,
#33801, and #33817.
littledivy pushed a commit to crowlKats/deno that referenced this pull request Jun 10, 2026
…and#33784)

Follow-up to denoland#33760 and denoland#33779. Converts all 8 ESM JS sources in
`ext/fetch` to IIFE-wrapped lazy-loaded scripts (`lazy_loaded_js`).

- `20_headers.js`, `21_formdata.js`, `22_body.js`, `22_http_client.js`,
`23_request.js`, `23_response.js`, `26_fetch.js`, `27_eventsource.js`
- `26_fetch.js` imports from `ext/telemetry` (TypeScript ESM, can't be
converted yet) -- bridged via `internals.__telemetry` /
`internals.__telemetryUtil` set in the telemetry source files
- All other cross-extension deps use `core.loadExtScript()` (ext/web,
ext/webidl, ext/net are already `lazy_loaded_js`)

This also eliminates the `internals.__fetchRequest/Response/Headers`
bridges from denoland#33778 since `ext/cache` can now use `core.loadExtScript()`
for ext/fetch directly.
littledivy pushed a commit to crowlKats/deno that referenced this pull request Jun 10, 2026
…d#33800)

Follow-up to denoland#33784. Converts `ext/fs/30_fs.js` from ESM to
IIFE-wrapped lazy-loaded script (`lazy_loaded_js`), eliminating ESM
module resolution overhead.

Updates three consumers to use `core.loadExtScript()`:
- `ext/process/40_process.js` (`FsFile` import)
- `ext/node/polyfills/_process/process.ts` (`fs` namespace import)
- `runtime/js/90_deno_ns.js` (`fs` namespace import)

No cross-extension blockers -- `30_fs.js` already uses
`core.loadExtScript()` for its `ext:deno_io` and `ext:deno_web`
dependencies (converted in denoland#33798).
littledivy pushed a commit to crowlKats/deno that referenced this pull request Jun 10, 2026
… scripts (denoland#33817)

- Convert `ext/process/40_process.js` from ESM to IIFE-wrapped
lazy-loaded script (`lazy_loaded_js`)
- Convert `ext/http/00_serve.ts`, `ext/http/01_http.js`,
`ext/http/02_websocket.ts` from ESM to IIFE-wrapped lazy-loaded scripts
- Update all consumers (`runtime/js/90_deno_ns.js`,
`runtime/js/99_main.js`, `ext/node/polyfills/child_process.ts`,
`ext/node/polyfills/internal/child_process.ts`) to use
`core.loadExtScript()` instead of ESM imports
- Replace `import.meta.log` calls with `internals.log` in `00_serve.ts`

Follows the same pattern established in denoland#33778, denoland#33780, denoland#33784, denoland#33800,
and denoland#33801.
littledivy pushed a commit to crowlKats/deno that referenced this pull request Jun 10, 2026
…ripts (denoland#33818)

## Summary

- Convert `ext/kv/01_db.ts` from ESM to IIFE-wrapped lazy-loaded script
(`lazy_loaded_js`)
- Convert `ext/webgpu/00_init.js` from ESM to IIFE-wrapped lazy-loaded
script (`lazy_loaded_js`)
- Update all consumers (`runtime/js/90_deno_ns.js`,
`runtime/js/98_global_scope_shared.js`,
`runtime/js/98_global_scope_window.js`,
`runtime/js/98_global_scope_worker.js`) to use `core.loadExtScript()`
instead of ESM imports
- Replace `import.meta.log` call with `internals.log` in `01_db.ts`

Follows the same pattern established in denoland#33778, denoland#33780, denoland#33784, denoland#33800,
denoland#33801, and denoland#33817.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants