Skip to content

perf(javascript): reduce JavascriptParser walk-path allocations#21139

Merged
alexander-akait merged 7 commits into
mainfrom
claude/javascriptparser-perf-ic0a98
Jun 9, 2026
Merged

perf(javascript): reduce JavascriptParser walk-path allocations#21139
alexander-akait merged 7 commits into
mainfrom
claude/javascriptparser-perf-ic0a98

Conversation

@alexander-akait

Copy link
Copy Markdown
Member

Cut per-node allocations on the JavascriptParser walk hot path:

  • replace the four memoize(() => arr.reverse()) member-info getters with a
    single-closure lazyReverse helper (one closure instead of two each);
  • hoist isSimpleFunction out of walkCallExpression so no closure is allocated
    per call expression, and replace .endsWith("FunctionExpression") checks with
    direct type comparisons;
  • skip the params spread in walkFunctionExpression for anonymous functions;
  • use an indexed loop in walkExpressions to avoid iterator allocation.

Measured over the three.js source tree (725 modules, walk phase): ~9.6% fewer
bytes allocated per pass and ~3-4% faster walk time.

Cut per-node allocations on the JavascriptParser walk hot path:
- replace the four memoize(() => arr.reverse()) member-info getters with a
  single-closure lazyReverse helper (one closure instead of two each);
- hoist isSimpleFunction out of walkCallExpression so no closure is allocated
  per call expression, and replace .endsWith("FunctionExpression") checks with
  direct type comparisons;
- skip the params spread in walkFunctionExpression for anonymous functions;
- use an indexed loop in walkExpressions to avoid iterator allocation.

Measured over the three.js source tree (725 modules, walk phase): ~9.6% fewer
bytes allocated per pass and ~3-4% faster walk time.
getMemberExpressionInfo allocated three arrays (members, optionals, ranges)
for every member expression before checking the root, but ~77% of calls
reject because the root is not a tracked variable. Add getMemberExpressionRoot
to resolve the root without allocating and only build the arrays on the
accepted path.

Collapse the callHooksForName/callHooksForInfo wrappers onto a shared
_callHooksForInfo core that takes the args array directly, so each call
collects the rest arguments once instead of through several forwarding layers.

Measured over the three.js source tree (walk phase): ~44% fewer bytes
allocated per pass and ~6-8% faster versus the previous baseline.
The ident handler in _preWalkVariableDeclaration only depends on the
loop-invariant hookMap, so build it once per declaration instead of once
per declarator.
…ling

Add a fast path in _preWalkVariableDeclaration for plain identifier
declarations (the common `const x =` form) that skips the enterPattern
dispatch and avoids the per-declaration onIdent closure, which is only
created now when a destructuring pattern is present.

Reuse a single instance-bound defineVariable callback for enterPatterns
instead of allocating an identical closure on every function/class/block
scope entry.

Walk phase over three.js: ~4-5% faster with no behavior change.
Gate the CALL-type getMemberExpressionInfo in walkCallExpression on a cheap
root check: it only yields a result for call-rooted chains (a().b()), which
is ~0.6% of member callees, so the common identifier/this-rooted callee now
skips the lookup. Also read the directive literal once in detectMode and use
an indexed loop in enterPatterns.
…mbers

tapEvaluateWithVariableInfo only populated its single-entry cache when getInfo
returned a result, so every member expression that failed variable resolution
recomputed getMemberExpressionInfo in both the default and stage-100 evaluate
taps. Cache the result unconditionally so the second tap reuses it, removing
~18% of getMemberExpressionInfo calls on three.js with no extra retained state.
@changeset-bot

changeset-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0b826b4

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 9, 2026

Copy link
Copy Markdown
Contributor

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

Install it locally:

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

@codecov

codecov Bot commented Jun 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.84946% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.35%. Comparing base (cd45931) to head (0b826b4).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
lib/javascript/JavascriptParser.js 97.84% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #21139   +/-   ##
=======================================
  Coverage   92.34%   92.35%           
=======================================
  Files         581      581           
  Lines       63228    63280   +52     
  Branches    17491    17508   +17     
=======================================
+ Hits        58390    58440   +50     
- Misses       4838     4840    +2     
Flag Coverage Δ
css-parsing 28.62% <3.22%> (-0.04%) ⬇️
html5lib 31.00% <3.22%> (-0.04%) ⬇️
integration 88.52% <97.84%> (+<0.01%) ⬆️
test262 45.29% <95.69%> (+<0.01%) ⬆️
unit 41.06% <81.72%> (-0.02%) ⬇️

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 9, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ 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

⚡ 4 improved benchmarks
❌ 3 regressed benchmarks
✅ 137 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "many-modules-commonjs", scenario '{"name":"mode-development","mode":"development"}' 921.8 KB 1,735.1 KB -46.87%
Memory benchmark "asset-modules-inline", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 220.5 KB 325.6 KB -32.28%
Memory benchmark "many-modules-esm", scenario '{"name":"mode-production","mode":"production"}' 7.2 MB 9.8 MB -26.23%
Memory benchmark "asset-modules-bytes", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 855.8 KB 323.1 KB ×2.6
Memory benchmark "react", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 344.6 KB 152.1 KB ×2.3
Memory benchmark "many-chunks-commonjs", scenario '{"name":"mode-production","mode":"production"}' 10 MB 6.9 MB +45.64%
Memory benchmark "css-modules", scenario '{"name":"mode-production","mode":"production"}' 9.3 MB 7.4 MB +26.16%

Tip

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


Comparing claude/javascriptparser-perf-ic0a98 (0b826b4) with main (8bdea16)

Open in CodSpeed

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging claude/javascriptparser-perf-ic0a98 into main will be
98.99%
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%206, 226
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.49%100%100%98.49%1577, 1873, 1880, 1888, 1910, 2806, 3249, 3924, 3954, 4007–4008, 4012, 4017, 4033–4034, 4048–4049, 4054–4055, 4532, 4558, 512, 517, 5366, 5398, 5415, 5431, 5447, 5462, 5487–5488, 5490, 5818, 5823, 5829, 5832, 5844, 5846, 5850, 5866, 5881, 5913, 5967, 5991, 6105, 731–732
   Compiler.js99.56%100%100%99.56%1135–1136, 1144
   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%170–171, 187, 206, 280
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.15%100%100%98.15%379, 425
   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.97%100%100%98.97%425–429, 577
   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.85%100%100%98.85%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%
   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%1311, 1316, 1376, 1390, 1452, 1461
   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%1219, 1222, 1239, 1256, 1503, 1537, 1553, 1640, 1994, 2292, 2297–2307, 417, 421, 575
   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.13%100%100%99.13%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%326
   cli.js98.62%100%100%98.62%10, 119, 545, 577, 627, 897
   index.js99.72%100%100%99.72%165
   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%
   AssetModulesPlugin.js97.32%100%100%97.32%283, 307, 310, 36, 362, 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.30%100%100%99.30%1428–1430, 1438, 273, 276, 281, 285
   normalization.js99.01%100%100%99.01%191–192, 258, 273
   target.js100%100%100%100%
lib/container
   ContainerEntryDependency.js100%100%100%100%
   ContainerEntryModule.js100%100%100%100%
   ContainerEntryModuleFactory.js100%100%100%100%
   ContainerExposedDependency.js100%100%100%100%
   ContainerPlugin.js100%100%100%100%
   ContainerReferencePlugin.js100%100%100%100%

@alexander-akait alexander-akait merged commit 3202f69 into main Jun 9, 2026
66 checks passed
@alexander-akait alexander-akait deleted the claude/javascriptparser-perf-ic0a98 branch June 9, 2026 10:34
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