perf(javascript): reduce JavascriptParser walk-path allocations#21139
Conversation
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 detectedLatest commit: 0b826b4 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 (3202f69). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@3202f69
yarn add -D webpack@https://pkg.pr.new/webpack@3202f69
pnpm add -D webpack@https://pkg.pr.new/webpack@3202f69 |
Codecov Report❌ Patch coverage is
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
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 not alter performance
|
| 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)
Cut per-node allocations on the JavascriptParser walk hot path:
single-closure lazyReverse helper (one closure instead of two each);
per call expression, and replace .endsWith("FunctionExpression") checks with
direct type comparisons;
Measured over the three.js source tree (725 modules, walk phase): ~9.6% fewer
bytes allocated per pass and ~3-4% faster walk time.