perf: reduce always-on overhead of the lazy barrel optimization#21213
Conversation
Free LazyBarrelInfo's export-name lookups (and drop the per-module LazyBarrelInfo reference) as soon as no re-export target stays deferred, so a resolved side-effect-free barrel no longer retains a per-export Map for the lifetime of the compilation.
Make dependency classification allocation-free: getLazyUntil now returns a
string tag instead of a fresh { id } / { local } object per dependency, with
the name read separately via getLazyName. classify() also skips allocating
LazyBarrelInfo for side-effect-free modules that defer nothing (e.g. leaf
modules with only local exports) and records terminal export names only when
a star re-export is present.
On a side-effect-free-heavy build this lowers CPU ~9% and retained heap ~2MB;
barrel builds are unchanged.
Two zero-behavior-change reductions on the always-on path: skip the `setLazy in dep` probe for the common non-deferrable dependency (only the rare deferrable branch needs it), and check `factoryMeta.sideEffectFree` before the WeakMap lookup in request() so non-side-effect-free modules return without a map probe (state only ever exists for side-effect-free modules). The full classify+request machinery measures <0.7% of build time.
A side-effect-free module's deferred side-effect imports keep their LazyBarrelInfo for the whole compilation when never requested by name, retaining a per-module Map/Set that is dead weight once the module graph is built. Clear the controller's per-module state in seal()'s final callback (lazy barrel only acts during make). On a 5000-module side-effect-free build this removes ~6MB of retained heap, bringing it back to (slightly below) the pre-feature baseline.
🦋 Changeset detectedLatest commit: 4308dcb The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
This PR is packaged and the instant preview is available (03063b8). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@03063b8
yarn add -D webpack@https://pkg.pr.new/webpack@03063b8
pnpm add -D webpack@https://pkg.pr.new/webpack@03063b8 |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #21213 +/- ##
==========================================
+ Coverage 92.73% 92.74% +0.01%
==========================================
Files 590 591 +1
Lines 64381 64362 -19
Branches 17858 17881 +23
==========================================
- Hits 59701 59695 -6
+ Misses 4680 4667 -13
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Merging this PR will degrade performance by 35.42%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Memory | benchmark "lodash", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
125.1 KB | 858.5 KB | -85.43% |
| ❌ | Memory | benchmark "asset-modules-inline", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
327 KB | 1,295.9 KB | -74.77% |
| ❌ | Memory | benchmark "wasm-modules-sync", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
129.3 KB | 357 KB | -63.78% |
| ❌ | Memory | benchmark "many-chunks-commonjs", scenario '{"name":"mode-production","mode":"production"}' |
7 MB | 10.4 MB | -32.44% |
| ⚡ | Memory | benchmark "side-effects-reexport", scenario '{"name":"mode-development","mode":"development"}' |
914.7 KB | 574.9 KB | +59.11% |
| ⚡ | Memory | benchmark "many-modules-esm", scenario '{"name":"mode-production","mode":"production"}' |
9.9 MB | 7.4 MB | +33.17% |
| ⚡ | Memory | benchmark "asset-modules-bytes", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
321.2 KB | 248.2 KB | +29.38% |
| ⚡ | Memory | benchmark "many-chunks-esm", scenario '{"name":"mode-production","mode":"production"}' |
9.1 MB | 7.4 MB | +22.64% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing claude/pr-21165-perf-memory-x1iq5b (4308dcb) with main (2b7d64e)
| if (state !== undefined) modules.delete(module); | ||
| return false; | ||
| } | ||
| // terminals only matter together with a star re-export (to avoid building it |
There was a problem hiding this comment.
I think we should always add addTerminal when lazyUntil === LAZY_UNTIL_LOCAL . The terminalId tells the importing module that the imported module already has a default export (which is terminal), so when forwardedIds: "default", there's no need to request other dependencies.
There was a problem hiding this comment.
@hai-x hm, looks like claude bug, can you send a PR?
There was a problem hiding this comment.
Sorry for the misleading, i just understand this purpose and logic. This seems to be to avoid creating unnecessary terminalId and the logic is also correct. We already have the covered test cases -> test/configCases/lazy-barrel/basic/mixed-barrel.
Summary
Follow-up perf/memory work on the lazy barrel optimization merged in #21165, which runs for every side-effect-free ESM module. This trims that always-on path: dependency classification is now allocation-free (a string tag +
getLazyName()instead of a fresh{ id }/{ local }object per dependency),LazyBarrelInfois allocated only when a module actually defers something, terminal export names are recorded only when a star re-export is present, the per-module lookups are released once targets are built, and the controller's per-module state is cleared after the make phase.Net: barrel-heavy builds keep the large win from #21165 (e.g. importing 3 of 600 exports: ~8x faster, ~45% lower peak RSS), while builds that don't benefit (side-effect-free heavy, no barrels) stay flat on CPU and flat-or-lower on retained memory. Refs #21165.
What kind of change does this PR introduce?
perf
Did you add tests for your changes?
No new tests — behavior is unchanged and is covered by the existing
test/configCases/lazy-barrel,test/watchCases/lazy-barrel, side-effects and stats suites (all passing).Does this PR introduce a breaking change?
No.
If relevant, what needs to be documented once your changes are merged or what have you already documented?
n/a
Use of AI
AI (Claude) was used to analyze #21165 for allocation/retention hotspots on the always-on path, implement these optimizations, and benchmark before/after; every change was measured and verified against the existing test suite.
Generated by Claude Code