Skip to content

refactor: reduce duplication and name magic constants across lib, speed up AggressiveMergingPlugin - #21525

Merged
alexander-akait merged 5 commits into
mainfrom
refactor/lib-dedupe-and-constants
Jul 27, 2026
Merged

refactor: reduce duplication and name magic constants across lib, speed up AggressiveMergingPlugin#21525
alexander-akait merged 5 commits into
mainfrom
refactor/lib-dedupe-and-constants

Conversation

@alexander-akait

Copy link
Copy Markdown
Member

Summary

Behavior-preserving cleanups across several lib/ modules: deduplicate repeated logic, replace magic numbers / char-codes with named constants, and flatten nested conditionals into guard clauses — in ResourceHintPlugin, RuntimePlugin, DefaultStatsPrinterPlugin, FileSystemInfo, ModuleConcatenationPlugin, HoistContainerReferencesPlugin, and IdHelpers.

Also includes two measured optimizations: AggressiveMergingPlugin now tracks the best chunk pair with a single scan instead of building and sorting the full O(chunks squared) pair list every pass (~20% faster on many-chunk builds using that plugin; the merged pair is identical, since a stable descending sort's first element is the first-encountered max), and Compilation releases its unsafe-cache restore tables in seal's finalCallback, next to the other make-phase bookkeeping already cleared there (they are only read during make).

No behavior change — the optimizations produce identical output.

What kind of change does this PR introduce?

refactor (primary); also a perf optimization (AggressiveMergingPlugin) and a memory release (Compilation).

Did you add tests for your changes?

No new tests — every change is behavior-preserving and already exercised by the existing suite. The full ConfigTestCases (8668 tests) and StatsTestCases (134 snapshots) pass unchanged, including the AggressiveMerging and splitChunks cases whose output is byte-identical.

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

Yes. Claude (via Claude Code) was used to survey lib/ for refactor candidates, implement the changes, and run the test suites plus CPU/memory benchmarks. Each change was reviewed for behavior preservation; the two optimizations were validated with before/after measurements (the ~20% AggressiveMerging figure came from strict-alternation benchmarking), and candidate changes that did not measure as wins were discarded rather than shipped.

🤖 Generated with Claude Code

https://claude.ai/code/session_01E6F4p5bU9iwDAcS23t4XjZ


Generated by Claude Code

The entrypoint-hint and HTML-hint collection paths each re-implemented
the same 4-level chunk × module × dependency walk with the same
triple-`instanceof` guard. Extract one `iterateHintedUrlAssets`
generator (with a typed `isUrlAssetDep` guard) that both consume, add an
`isCssOrHtmlUrlAssetDep` guard plus early-exit for the runtime-requirement
pass, name the JS-chunk-file regexp, and spell out the loop variables. No
behavior change.
Behavior-preserving cleanups across four modules:

- RuntimePlugin: table-drive the ~15 mechanical `runtimeRequirementInTree`
  taps that only attach a zero-arg runtime module, and use PLUGIN_NAME for
  the two hardcoded tap names.
- DefaultStatsPrinterPlugin: collapse the 12 identical `*_SIMPLE_PRINTERS`
  registration loops into one loop over the printer maps.
- FileSystemInfo: name the Snapshot `_flags` bits (a serialized-cache format
  contract) instead of inline `0x10`..`0x1000` masks.
- ModuleConcatenationPlugin: extract the 8 repeated failure-cache bail tails
  in `_tryToAdd` into a module-level helper (off the hot success path).
- HoistContainerReferencesPlugin: share one callback across the two
  federation dependency hooks and extract the duplicated runtime-chunk
  hoist loop into `hoistReferencedModules`.
- IdHelpers: name the char-code and length literals in `avoidNumber` /
  `shortenLongString`. Byte-identical.
…che tables

- AggressiveMergingPlugin only merges the single best chunk pair per pass
  (the hook re-runs after each merge), so track the max pair with a strict
  `>` scan instead of building and sorting the full O(chunks²) pair list
  every pass. Same pair chosen (a stable descending sort's `[0]` is the
  first-encountered max); no per-pass array allocation. ~20% faster on
  many-chunk builds using this plugin.
- Compilation: clear `_restoredUnsafeCacheModuleEntries` /
  `_restoredUnsafeCacheEntries` in seal's finalCallback, next to the other
  make-phase bookkeeping already released there — they are only read during
  make.
@cursor

cursor Bot commented Jul 27, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches core compilation, chunk optimization, runtime injection, and federation hoisting, but the PR positions everything as behavior-preserving with identical test output; the main risk is subtle equivalence bugs in merging selection or hoist paths.

Overview
Behavior-preserving refactors across lib/ plus two targeted wins: faster chunk merging and lower post-make memory from clearing unsafe-cache restore tables.

AggressiveMergingPlugin no longer builds and sorts every chunk pair each pass—it tracks the single best pair in one O(chunks²) scan (same merge choice as before via strict > / first-encountered max).

Compilation clears _restoredUnsafeCacheModuleEntries and _restoredUnsafeCacheEntries in seal’s finalCallback alongside other make-phase bookkeeping so restored modules aren’t pinned for the rest of the compilation.

FileSystemInfo documents Snapshot._flags with named SNAPSHOT_FLAG_* constants (persisted-cache contract; values unchanged). IdHelpers replaces magic lengths and char codes with named constants.

RuntimePlugin registers simple zero-arg tree runtime modules via SIMPLE_TREE_RUNTIME_MODULES instead of many duplicate taps; conditional handlers stay explicit. DefaultStatsPrinterPlugin registers all *_SIMPLE_PRINTERS maps in one loop.

HoistContainerReferencesPlugin shares a traceDep callback and extracts hoistReferencedModules for duplicated hoist logic. ModuleConcatenationPlugin centralizes failure caching in cacheFailure. ResourceHintPlugin adds shared helpers (iterateHintedUrlAssets, type guards, JS_CHUNK_FILE_REGEXP) to dedupe chunk×module×dependency walks.

Patch changeset only; no public API changes.

Reviewed by Cursor Bugbot for commit b35dc23. Bugbot is set up for automated code reviews on this repo. Configure here.

@changeset-bot

changeset-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b35dc23

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack Patch

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

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

This PR is packaged and the instant preview is available (e28262e).

Install it locally:

  • npm
npm i -D webpack@https://pkg.pr.new/webpack@e28262e
  • yarn
yarn add -D webpack@https://pkg.pr.new/webpack@e28262e
  • pnpm
pnpm add -D webpack@https://pkg.pr.new/webpack@e28262e

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1956798. Configure here.

Comment thread .changeset/062-perf-aggressive-merging.md Outdated
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.79%. Comparing base (3151fa6) to head (b35dc23).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff            @@
##             main   #21525    +/-   ##
========================================
  Coverage   93.79%   93.79%            
========================================
  Files         620      620            
  Lines       73761    73276   -485     
  Branches    21289    21181   -108     
========================================
- Hits        69183    68732   -451     
+ Misses       4578     4544    -34     
Flag Coverage Δ
css-parsing 25.59% <44.53%> (+0.05%) ⬆️
html5lib 27.02% <44.53%> (+0.06%) ⬆️
integration 89.60% <100.00%> (+0.01%) ⬆️
test262 43.34% <43.69%> (+0.05%) ⬆️
unit 47.24% <47.89%> (-0.21%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq

codspeed-hq Bot commented Jul 27, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 20.09%

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 214 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "asset-modules-resource", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 635.8 KB 1,202.6 KB -47.14%
Memory benchmark "future-defaults", scenario '{"name":"mode-production","mode":"production"}' 10.6 MB 8.8 MB +20.79%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing refactor/lib-dedupe-and-constants (b35dc23) with main (84d351c)

Open in CodSpeed

@github-actions

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging refactor/lib-dedupe-and-constants into main will be
99.33%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
bin
   webpack.js98.77%100%100%98.77%91
examples
   build-common.js100%100%100%100%
   buildAll.js100%100%100%100%
   examples.js100%100%100%100%
   template-common.js98.21%100%100%98.21%72
examples/custom-javascript-parser
   test.filter.js100%100%100%100%
examples/custom-javascript-parser/internals
   acorn-parse.js100%100%100%100%
   meriyah-parse.js100%100%100%100%
   oxc-parse.js100%100%100%100%
examples/markdown
   webpack.config.mjs100%100%100%100%
examples/module-federation
   test.filter.js100%100%100%100%
examples/reexport-components
   test.filter.js100%100%100%100%
examples/typescript
   test.filter.js100%100%100%100%
examples/typescript-non-erasable
   test.filter.js50%100%100%50%5
examples/virtual-modules
   test.filter.js100%100%100%100%
examples/wasm-bindgen-esm
   test.filter.js100%100%100%100%
examples/wasm-complex
   test.filter.js100%100%100%100%
examples/wasm-emscripten
   test.filter.js100%100%100%100%
examples/wasm-simple
   test.filter.js100%100%100%100%
examples/wasm-simple-source-phase
   test.filter.js100%100%100%100%
lib
   APIPlugin.js100%100%100%100%
   AsyncDependenciesBlock.js100%100%100%100%
   AutomaticPrefetchPlugin.js100%100%100%100%
   BannerPlugin.js100%100%100%100%
   Cache.js98.21%100%100%98.21%101
   CacheFacade.js100%100%100%100%
   Chunk.js99.72%100%100%99.72%39
   ChunkGraph.js100%100%100%100%
   ChunkGroup.js100%100%100%100%
   ChunkTemplate.js100%100%100%100%
   CircularModulesPlugin.js98.81%100%100%98.81%136
   CleanPlugin.js99.12%100%100%99.12%207, 227
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.42%100%100%98.42%1639, 1958, 1965, 1973, 1995, 1998, 2937, 3416–3417, 3449, 4153, 4183, 4236–4237, 4241, 4246, 4262–4263, 4277–4278, 4283–4284, 4764, 4790, 527, 532, 5598, 5630, 5647, 5663, 5679, 5694, 5719–5720, 5722, 6052, 6057, 6063, 6066, 6073, 6085, 6087, 6091, 6107, 6122, 6154, 6208, 6232, 6347, 778–779
   Compiler.js99.56%100%100%99.56%1147–1148, 1156
   ConcatenationScope.js98.65%100%100%98.65%195
   ConditionalInitFragment.js100%100%100%100%
   ConstPlugin.js100%100%100%100%
   ContextExclusionPlugin.js100%100%100%100%
   ContextModule.js99.88%100%100%99.88%1461
   ContextModuleFactory.js97.20%100%100%97.20%266, 435, 456, 461, 501, 512, 514, 518, 527–528
   ContextReplacementPlugin.js100%100%100%100%
   DefinePlugin.js99.08%100%100%99.08%1074, 176–177, 193, 212, 286
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.51%100%100%98.51%479, 525
   DependencyTemplate.js100%100%100%100%
   DependencyTemplates.js100%100%100%100%
   DotenvPlugin.js98.41%100%100%98.41%378, 391–392
   DynamicEntryPlugin.js100%100%100%100%
   EntryOptionPlugin.js100%100%100%100%
   EntryPlugin.js100%100%100%100%
   Entrypoint.js100%100%100%100%
   EnvironmentPlugin.js97.14%100%100%97.14%49
   ErrorHelpers.js100%100%100%100%
   EvalDevToolModulePlugin.js100%100%100%100%
   EvalSourceMapDevToolPlugin.js100%100%100%100%
   ExportsInfo.js100%100%100%100%
   ExportsInfoApiPlugin.js100%100%100%100%
   ExternalModule.js98.65%100%100%98.65%1196, 1199, 514–518, 520, 666
   ExternalModuleFactoryPlugin.js100%100%100%100%
   ExternalsPlugin.js100%100%100%100%
   FileSystemInfo.js99.53%100%100%99.53%182, 2418–2419, 2422, 2433, 2444, 2455, 280, 3892, 3907, 3931
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.21%100%100%98.21%448, 457, 460, 464, 476
   FlagDependencyUsagePlugin.js100%100%100%100%
   FlagEntryExportAsUsedPlugin.js100%100%100%100%
   Generator.js100%100%100%100%
   HotModuleReplacementPlugin.js100%100%100%100%
   HotUpdateChunk.js100%100%100%100%
   IgnorePlugin.js100%100%100%100%
   IgnoreWarningsPlugin.js100%100%100%100%
   InitFragment.js100%100%100%100%
   JavascriptMetaInfoPlugin.js100%100%100%100%
   LazyBarrel.js100%100%100%100%
   LibraryTemplatePlugin.js100%100%100%100%
   LoaderOptionsPlugin.js100%100%100%100%
   LoaderTargetPlugin.js100%100%100%100%
   MainTemplate.js100%100%100%100%
   ManifestPlugin.js100%100%100%100%
   Module.js98.50%100%100%98.50%1288, 1293, 1353, 1367, 1429, 1438
   ModuleFactory.js100%100%100%100%
   ModuleFilenameHelpers.js98.85%100%100%98.85%106, 108
   ModuleGraph.js99.73%100%100%99.73%1005
   ModuleGraphConnection.js100%100%100%100%
   ModuleInfoHeaderPlugin.js100%100%100%100%
   ModuleNotFoundError.js100%100%100%100%
   ModuleProfile.js100%100%100%100%
   ModuleSourceTypeConstants.js100%100%100%100%
   ModuleTemplate.js100%100%100%100%
   ModuleTypeConstants.js100%100%100%100%
   MultiCompiler.js99.70%100%100%99.70%663
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js97.98%100%100%97.98%1014, 1031, 1279, 1313, 1329, 1776, 2073, 2078–2088, 34, 994, 997
   NormalModuleFactory.js98.72%100%100%98.72%1117, 1385, 1396, 1406, 1457–1459, 1466, 520, 532
   NormalModuleReplacementPlugin.js100%100%100%100%
   NullFactory.js100%100%100%100%
   OptimizationStages.js100%100%100%100%
   OptionsApply.js100%100%100%100%
   Parser.js100%100%100%100%
   PlatformPlugin.js100%100%100%100%
   PrefetchPlugin.js100%100%100%100%
   ProgressPlugin.js99.80%100%100%99.80%691
   ProvidePlugin.js100%100%100%100%
   RawModule.js100%100%100%100%
   RecordIdsPlugin.js100%100%100%100%
   RequestShortener.js100%100%100%100%
   ResolverFactory.js100%100%100%100%
   RuntimeGlobals.js100%100%100%100%
   RuntimeModule.js100%100%100%100%
   RuntimePlugin.js100%100%100%100%
   RuntimeTemplate.js100%100%100%100%
   SelfModuleFactory.js100%100%100%100%
   SingleEntryPlugin.js100%100%100%100%
   SourceMapDevToolModuleOptionsPlugin.js100%100%100%100%
   SourceMapDevToolPlugin.js98.62%100%100%98.62%220, 224, 226, 419, 430, 889
   Stats.js100%100%100%100%
   Template.js100%100%100%100%
   TemplatedPathPlugin.js99.43%100%100%99.43%308–309
   UseStrictPlugin.js100%100%100%100%
   WarnCaseSensitiveModulesPlugin.js100%100%100%100%
   WarnDeprecatedOptionPlugin.js100%100%100%100%
   WarnNoModeSetPlugin.js100%100%100%100%
   WatchIgnorePlugin.js100%100%100%100%
   Watching.js100%100%100%100%
   WebpackError.js100%100%100%100%
   WebpackIsIncludedPlugin.js100%100%100%100%
   WebpackOptionsApply.js100%100%100%100%
   WebpackOptionsDefaulter.js100%100%100%100%
   buildChunkGraph.js99.87%100%100%99.87%371
   cli.js98.63%100%100%98.63%10, 119, 549, 581, 631, 905
   index.js99.72%100%100%99.72%184
   validateSchema.js94.67%100%100%94.67%100, 87, 89, 98
   webpack.js97.10%100%100%97.10%10, 263, 285, 287
lib/asset
   AssetBytesGenerator.js100%100%100%100%
   AssetBytesParser.js100%100%100%100%
   AssetGenerator.js100%100%100%100%
   AssetModule.js100%100%100%100%
   AssetModulesPlugin.js97.95%100%100%97.95%295, 319, 322, 42, 452, 47
   AssetParser.js100%100%100%100%
   AssetSourceGenerator.js100%100%100%100%
   AssetSourceParser.js100%100%100%100%
   RawDataUrlModule.js100%100%100%100%
   WebManifestGenerator.js100%100%100%100%
   WebManifestParser.js100%100%100%100%
lib/async-modules
   AsyncModuleHelpers.js100%100%100%100%
   AwaitDependenciesInitFragment.js100%100%100%100%
   InferAsyncModulesPlugin.js100%100%100%100%
   isGeneratorLowered.js100%100%100%100%
lib/bun
   BunTargetPlugin.js100%100%100%100%
lib/cache
   AddBuildDependenciesPlugin.js100%100%100%100%
   AddManagedPathsPlugin.js100%100%100%100%
   IdleFileCachePlugin.js97.92%100%100%97.92%75, 87, 95
   MemoryCachePlugin.js95.83%100%100%95.83%33
   MemoryWithGcCachePlugin.js93.15%100%100%93.15%107, 114–115, 123, 90
   PackFileCacheStrategy.js96.41%100%100%96.41%1257, 1357, 1361, 1423, 628, 647, 657–659, 661, 677–678, 683, 686, 688, 693, 698, 723, 729, 763, 769, 775, 780, 791, 800, 805–806, 808, 825, 831–832, 834
   ResolverCachePlugin.js100%100%100%100%
   getLazyHashedEtag.js100%100%100%100%
   mergeEtags.js100%100%100%100%
lib/config
   browserslistTargetHandler.js100%100%100%100%
   defaults.js99.37%100%100%99.37%1620–1622, 1630,

Copy link
Copy Markdown
Member Author

Regarding the CodSpeed memory report — I believe the flagged regression is measurement noise, not a real effect:

  • The report flipped between the last two runs even though the code is unchanged. The previous run (1956798) reported an overall ×2.4 improvement (e.g. wasm-modules-async rebuild 814 KB → 195 KB); this run (b35dc23) reports a ~20% regression on a different benchmark (asset-modules-resource rebuild 635 KB → 1202 KB). b35dc23 only edits .changeset/*.md — its lib/ code is byte-for-byte identical to 1956798 — so these sub-MB, watch-rebuild memory benchmarks are dominated by GC/sampling variance (the report also notes "different runtime environments" and, earlier, no successful base run on main).
  • The one large, stable benchmark is consistent in both runs: future-defaults production memory improves (9.6 → 7.2 MB, then 10.6 → 8.8 MB). That's the intended effect of clearing the unsafe-cache restore tables after seal.

None of the changes here plausibly increase asset-module rebuild memory (ResourceHintPlugin isn't exercised without resourceHints; the Compilation change only releases memory). So the net memory effect looks like an improvement and the asset-modules-resource delta looks like noise. Happy to acknowledge it on CodSpeed if you'd prefer.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

The runtimes (bun) failure is pre-existing on the base branch and unrelated to this PR — this diff touches zero serialization code. The single failing test is BinaryMiddleware › invalid streams › should throw on a payload from a newer V8 format version, added by #21514 (feat(perf): use v8 for serialization) which merged into main today; CI runs the PR merged into main, so this lane inherited it.

Root cause: the test crafts a payload via v8.serialize([1, "x"]) then sets payload[1] = 0x7f, expecting the version guard to throw. That guard is if (payload[0] === 0xff && payload[1] > V8_FORMAT_VERSION). On Node, v8.serialize emits the V8 wire format ([0xff, 15, …]), so the guard fires. Under Bun, node:v8 uses JavaScriptCore's format (v8.serialize(null)[13, 0, 0, 0, 4], i.e. payload[0] !== 0xff), so the guard is skipped and nothing throws — the toThrow assertion fails.

Everything else is green (full integration matrix across all OS/Node versions, Codecov patch + project). Will re-run once the base branch's Bun lane is fixed.


Generated by Claude Code

@alexander-akait
alexander-akait merged commit e28262e into main Jul 27, 2026
62 of 63 checks passed
@alexander-akait
alexander-akait deleted the refactor/lib-dedupe-and-constants branch July 27, 2026 01:24
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.

1 participant