perf: read each wrapped module from disk once#265
Draft
BridgeAR wants to merge 3 commits into
Draft
Conversation
bengl
reviewed
Jun 30, 2026
98b4edd to
9eace65
Compare
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.
9eace65 to
7bcd78f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
emits then does
import * as namespace from <realUrl>, so Node reads thesame file again to compile that import. Stash the source the export pass
fetched and serve it to the namespace import.
export *barrels is lexed once per barrel eventhough 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.formatand resolves re-exports per call, and built-ins alreadymemoize via BUILT_INS.
Test plan
module.registerHooksloader (Node 24), plus the TypeScript andmultiple-loaders suites.
cache and memo active.
Stacked on #264; the first two commits are that PR.