Skip to content

perf: keep dependency source locations lazy when sorting dependencies#21228

Merged
alexander-akait merged 3 commits into
mainfrom
claude/webpack-issue-15643-oy9mgi
Jun 20, 2026
Merged

perf: keep dependency source locations lazy when sorting dependencies#21228
alexander-akait merged 3 commits into
mainfrom
claude/webpack-issue-15643-oy9mgi

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary

Profiling a parse-heavy build showed NormalModule.handleParseResult sorting each module's dependencies with compareSelect(d => d.loc, compareLocations). get loc rebuilds and caches the location object, so this sort materialized and retained a loc object on every dependency — even with source maps off — which defeats the lazy-loc memory optimization from #21183 and allocates during the sort.

This adds Dependency.compareLocations, which compares via the stored numeric loc fields without materializing loc, and setLocWithIndex, which replaces the dep.loc = Object.create(loc); dep.loc.index = i pattern (8 sites in the harmony-export and HMR parser plugins) so the index lives in the serialized field and loc stays lazy. Behavior and output are unchanged.

Measured on a synthetic 1500-module, parse-heavy build (mode: "production", devtool: false, no cache), cold runs in separate processes:

Metric Before After Δ
Dependencies retaining a loc object 19,657 (100%) 0 (0%) −100%
Build time, median of 9 1821 ms 1704 ms −6.4%
Build time, best of 9 1753 ms 1627 ms −7.2%
Heap used after build, median of 9 58.7 MB 54.9 MB −6.5%

This branch also includes a small regression test (test/configCases/lazy-barrel/skip-build) asserting a side-effect-free barrel's unused subtree is never built or run through loaders (relates to #15643).

What kind of change does this PR introduce?

perf

Did you add tests for your changes?

Yes — test/configCases/dependency-loc-sort exercises the harmony re-export setLocWithIndex sites and the loc-based dependency sort, asserting both the re-export values and source-order execution of the imported modules. The change is otherwise behavior-preserving and covered by the existing StatsTestCases/ConfigTestCases suites, which I ran with results identical to main.

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. An AI assistant (Claude) was used to CPU-profile the build, identify the loc-materialization hot path, implement the change, and verify equivalence against the existing test suites; all output was reviewed.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yBjCfSr5bwVz6JsxpW85m

@changeset-bot

changeset-bot Bot commented Jun 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d78a954

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 Jun 20, 2026

Copy link
Copy Markdown
Contributor

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

Install it locally:

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

@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.76%. Comparing base (40b972f) to head (d78a954).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #21228      +/-   ##
==========================================
- Coverage   92.77%   92.76%   -0.02%     
==========================================
  Files         591      591              
  Lines       64488    64492       +4     
  Branches    17920    17926       +6     
==========================================
- Hits        59829    59825       -4     
- Misses       4659     4667       +8     
Flag Coverage Δ
css-parsing 28.72% <4.76%> (+<0.01%) ⬆️
html5lib 31.17% <4.76%> (+<0.01%) ⬆️
integration 88.74% <100.00%> (-0.02%) ⬇️
test262 45.51% <90.47%> (+0.01%) ⬆️
unit 41.13% <4.76%> (+<0.01%) ⬆️

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 Jun 20, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 40.71%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 5 regressed benchmarks
✅ 138 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "asset-modules-bytes", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 247.3 KB 1,045.4 KB -76.34%
Memory benchmark "wasm-modules-sync", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 126.2 KB 254.2 KB -50.34%
Memory benchmark "wasm-modules-async", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 186.7 KB 348.1 KB -46.37%
Memory benchmark "many-modules-esm", scenario '{"name":"mode-production","mode":"production"}' 7.4 MB 10.4 MB -28.48%
Memory benchmark "css-modules", scenario '{"name":"mode-production","mode":"production"}' 7.1 MB 9.1 MB -22.28%
Memory benchmark "many-chunks-commonjs", scenario '{"name":"mode-production","mode":"production"}' 9.4 MB 7.6 MB +24.04%

Tip

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


Comparing claude/webpack-issue-15643-oy9mgi (d78a954) with main (40b972f)

Open in CodSpeed

@alexander-akait alexander-akait force-pushed the claude/webpack-issue-15643-oy9mgi branch 3 times, most recently from 3efd978 to 9960737 Compare June 20, 2026 11:02
Add a config case where a side-effect-free node_modules barrel re-exports
a used and an unused target; the unused target pulls in a deep subtree
guarded by a loader that throws if it runs. Verifies the unused subtree is
never factorized, built, or run through loaders when only the other export
is imported.
NormalModule sorts each module's dependencies by source location using
`compareSelect(d => d.loc, compareLocations)`. `get loc` rebuilds and caches
the location object, so this sort materialized and retained a loc object on
every dependency (even with source maps off), defeating the lazy-loc memory
optimization and allocating during the sort.

Add `Dependency.compareLocations`, comparing via the stored numeric loc
fields without materializing `loc`, and `setLocWithIndex` to replace the
`dep.loc = Object.create(loc); dep.loc.index = i` pattern so the index lives
in the serialized field and loc stays lazy.
Add test/configCases/dependency-loc-sort exercising the harmony re-export
setLocWithIndex sites and the loc-based dependency sort, asserting both the
re-export values and source-order execution of the imported modules.
@alexander-akait alexander-akait force-pushed the claude/webpack-issue-15643-oy9mgi branch from 9960737 to d78a954 Compare June 20, 2026 19:29
@github-actions

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging claude/webpack-issue-15643-oy9mgi into main will be
99.35%
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.js91.30%100%100%91.30%140, 142–143, 145, 147, 153–154, 161, 168, 90
examples/markdown
   webpack.config.mjs100%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-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%
   CleanPlugin.js99.15%100%100%99.15%207, 227
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.43%100%100%98.43%1618, 1937, 1944, 1952, 1974, 1977, 2917, 3396–3397, 3429, 4095, 4125, 4178–4179, 4183, 4188, 4204–4205, 4219–4220, 4225–4226, 4703, 4729, 514, 519, 5537, 5569, 5586, 5602, 5618, 5633, 5658–5659, 5661, 5989, 5994, 6000, 6003, 6015, 6017, 6021, 6037, 6052, 6084, 6138, 6162, 6276, 764–765
   Compiler.js99.56%100%100%99.56%1139–1140, 1148
   ConcatenationScope.js98.59%100%100%98.59%189
   ConditionalInitFragment.js100%100%100%100%
   ConstPlugin.js100%100%100%100%
   ContextExclusionPlugin.js100%100%100%100%
   ContextModule.js100%100%100%100%
   ContextModuleFactory.js97.40%100%100%97.40%258, 395, 418, 420, 424, 433–434
   ContextReplacementPlugin.js100%100%100%100%
   DefinePlugin.js99%100%100%99%171–172, 188, 207, 281
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.50%100%100%98.50%471, 517
   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.50%100%100%98.50%1057, 1060, 445–449, 451, 597
   ExternalModuleFactoryPlugin.js100%100%100%100%
   ExternalsPlugin.js100%100%100%100%
   FileSystemInfo.js99.50%100%100%99.50%182, 2252–2253, 2256, 2267, 2278, 2289, 278, 3693, 3708, 3732
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.46%100%100%98.46%425, 434, 436, 440
   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%1285, 1290, 1350, 1364, 1426, 1435
   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.69%100%100%99.69%659
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js97.90%100%100%97.90%1237, 1240, 1257, 1274, 1521, 1555, 1571, 1658, 2014, 2313, 2318–2328, 418, 422, 576
   NormalModuleFactory.js99.47%100%100%99.47%1083, 1392, 486, 498
   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.js98.85%100%100%98.85%519–520, 525, 527, 591
   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, 891
   Stats.js100%100%100%100%
   Template.js100%100%100%100%
   TemplatedPathPlugin.js99.17%100%100%99.17%176–177
   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.62%100%100%98.62%10, 119, 545, 577, 627, 897
   index.js99.72%100%100%99.72%184
   validateSchema.js94.67%100%100%94.67%100, 87, 89, 98
   webpack.js96.33%100%100%96.33%10, 198, 220, 222
lib/asset
   AssetBytesGenerator.js100%100%100%100%
   AssetBytesParser.js100%100%100%100%
   AssetGenerator.js100%100%100%100%
   AssetModule.js100%100%100%100%
   AssetModulesPlugin.js97.32%100%100%97.32%281, 305, 308, 36, 360, 41
   AssetParser.js100%100%100%100%
   AssetSourceGenerator.js100%100%100%100%
   AssetSourceParser.js100%100%100%100%
   RawDataUrlModule.js100%100%100%100%
lib/async-modules
   AsyncModuleHelpers.js100%100%100%100%
   AwaitDependenciesInitFragment.js100%100%100%100%
   InferAsyncModulesPlugin.js100%100%100%100%
lib/cache
   AddBuildDependenciesPlugin.js100%100%100%100%
   AddManagedPathsPlugin.js100%100%100%100%
   IdleFileCachePlugin.js97.92%100%100%97.92%71, 83, 91
   MemoryCachePlugin.js95.83%100%100%95.83%33
   MemoryWithGcCachePlugin.js93.15%100%100%93.15%106, 113–114, 122, 89
   PackFileCacheStrategy.js96.40%100%100%96.40%1250, 1350, 1354, 1416, 628, 647, 657–659, 661, 677–678, 683, 686, 688, 693, 698, 722, 728, 762, 768, 774, 779, 790, 799, 804–805, 807, 824, 830–831, 833
   ResolverCachePlugin.js100%100%100%100%
   getLazyHashedEtag.js100%100%100%100%
   mergeEtags.js100%100%100%100%
lib/config
   browserslistTargetHandler.js100%100%100%100%
   defaults.js99.31%100%100%99.31%1444–1446, 1454, 274, 277, 282, 286
   defineConfig.js100%100%100%100%
   normalization.js99.02%100%100%99.02%191–192, 258, 273
   target.js100%100%100%100%
lib/container
   

@alexander-akait alexander-akait merged commit 95ea1f1 into main Jun 20, 2026
67 of 68 checks passed
@alexander-akait alexander-akait deleted the claude/webpack-issue-15643-oy9mgi branch June 20, 2026 22:03
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