Skip to content

perf: read each wrapped module from disk once#265

Draft
BridgeAR wants to merge 3 commits into
nodejs:mainfrom
BridgeAR:BridgeAR/2026-06-30-dedup-source-reads
Draft

perf: read each wrapped module from disk once#265
BridgeAR wants to merge 3 commits into
nodejs:mainfrom
BridgeAR:BridgeAR/2026-06-30-dedup-source-reads

Conversation

@BridgeAR

Copy link
Copy Markdown
Member

Summary

Wrapping a module reads its source twice and re-lexes shared leaves once per
barrel. Two changes remove both, with no observable behavior change:

  1. The export pass reads a module's source to lex its exports; the wrapper it
    emits then does import * as namespace from <realUrl>, so Node reads the
    same file again to compile that import. Stash the source the export pass
    fetched and serve it to the namespace import.
  2. A leaf reached from several export * barrels is lexed once per barrel even
    though its export set is intrinsic to the file. Memoize the lexed ESM export
    names by URL so the first lex serves the rest.

On a 390-module barrel-heavy graph under the synchronous loader: export lexes
588 → 396 (-33%), total module read calls 1970 → 794 (-60%).

The source cache only holds entries whose upstream load supplied a format; a
load that returns source without one relies on Node to resolve the format on
the real load, and serving such an entry would throw ERR_UNKNOWN_MODULE_FORMAT.
The export memo only caches the pure ESM result — the CommonJS path mutates
context.format and resolves re-exports per call, and built-ins already
memoize via BUILT_INS.

Test plan

  • Full suite green on the off-thread loader (Node 22) and the synchronous
    module.registerHooks loader (Node 24), plus the TypeScript and
    multiple-loaders suites.
  • Star cycles still terminate (graceful wrap-failure fallback) with the source
    cache and memo active.

Stacked on #264; the first two commits are that PR.

Comment thread create-hook.mjs Outdated
@BridgeAR BridgeAR force-pushed the BridgeAR/2026-06-30-dedup-source-reads branch 2 times, most recently from 98b4edd to 9eace65 Compare June 30, 2026 19:32
BridgeAR added 3 commits June 30, 2026 22:33
Wrapping a module reads its source to lex the exports, and the wrapper it
emits then does `import * as namespace from <realUrl>`, so Node reads the same
file again to compile that import. Stash the source the export pass already
fetched and serve it to the namespace import, so each wrapped module is read
once instead of twice. On a 390-module barrel-heavy graph under the synchronous
loader, total module read calls drop from 1970 to 1178 (-40%).

Cache ESM only. A wrapped CJS module's real load is served by the
`cjsInIitmChain` branch, which returns `source: undefined` so Node's native CJS
loader handles "module-sync" require chains. That branch never reads the cache,
so a cached CJS entry could only retain memory, never be consumed; reordering
the two so CJS gets cached would reintroduce that retention. A usable format is
also required: a load returning source without one relies on Node to resolve the
format on the real load, and short-circuiting it with a formatless cache entry
throws ERR_UNKNOWN_MODULE_FORMAT.
A leaf module reached from several `export *` barrels is read and lexed once
per barrel, even though its export set is intrinsic to the file. Cache the
lexed ESM export names by URL so the first lex serves every later barrel that
re-exports the same leaf. On a 390-module barrel-heavy graph the export lexes
drop from 588 to 396 (-33%), and combined with serving the wrapper's namespace
import from the export-pass read, total module reads drop from 1970 to 794
(-60%).

Only the pure ESM result is cached. The CommonJS path mutates context.format
and resolves re-exports per call, and built-ins already memoize via BUILT_INS,
so both keep their existing behavior.
The export memo returns a leaf's export set from cache once a second barrel
re-exports the same leaf. The bindings are imported through both barrels so a
wrong memoized set shows up as a missing re-export; the existing star-re-export
test re-exports each leaf once and never reaches the memo hit.
@BridgeAR BridgeAR force-pushed the BridgeAR/2026-06-30-dedup-source-reads branch from 9eace65 to 7bcd78f Compare June 30, 2026 20:36
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