perf: monomorphic ResolveRequest clones via cloneRequest helper#530
perf: monomorphic ResolveRequest clones via cloneRequest helper#530alexander-akait wants to merge 3 commits into
Conversation
Rewrites the `{ ...request, ... }` spread pattern used across every
request-producing plugin into a shared `cloneRequest` helper that writes
the canonical ResolveRequest fields as a fixed-order object literal.
V8 can then cache a single hidden class for the result, matching the
perf win from #445 (ParsePlugin, JoinRequestPlugin, JoinRequestPartPlugin)
without dropping unknown own properties — after the literal, any extra
string or symbol keys on the original request are copied over, so
plugin authors (e.g. webpack's ResolverCachePlugin) that attach custom
state to the request keep working unchanged.
|
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #530 +/- ##
==========================================
- Coverage 96.75% 96.72% -0.04%
==========================================
Files 50 50
Lines 2589 2592 +3
Branches 788 790 +2
==========================================
+ Hits 2505 2507 +2
- Misses 69 70 +1
Partials 15 15
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Merging this PR will improve performance by 66.75%
Performance Changes
Comparing |
This reverts commit 39c6b7a.
…oResolve `doResolve` ran on every plugin step (~10-15 per resolve), and both of the per-call costs stacked up under the CodSpeed `--no-opt` flags: - The recursion-detection Set was cloned on every step via `new Set(resolveContext.stack)`, giving O(depth^2) Set work per top-level resolve. Switched to one Set per top-level resolve with add-on-entry / delete-on-exit around the hook call. Recursion semantics are preserved because the Set mirrors the live path. - When `resolveContext.log` is not set (the production hot path), the previous code still allocated a fresh options literal and ran `createInnerContext` (which returned yet another literal). With the stack now shared by reference, the inner context is structurally identical to the parent; forward it directly and only fall through to `createInnerContext` when log wrapping is actually needed. Benchmark (wall-clock under the package's `--no-opt --predictable` flags, branch vs main): pathological-deep-stack: +31.2% (13.3 -> 9.16 ms) alias-realistic: +28.6% array-alias: +24.0% multiple-modules: +9.9% symlinks (off): +10.1% alias-field: +8.4% realistic-midsize (warm): +7.2% main-field: +6.1% description-files-multi: +6.0% mixed-conditions: +5.4% query-fragment: +4.9% concurrent-batch: +3.4% The pathological-deep-stack case is the direct validation of the Set change: with a 50-deep alias chain, removing the O(N^2) snapshotting matches the theoretical win exactly.
Rewrites the
{ ...request, ... }spread pattern used across everyrequest-producing plugin into a shared
cloneRequesthelper that writesthe canonical ResolveRequest fields as a fixed-order object literal.
V8 can then cache a single hidden class for the result, matching the
perf win from #445 (ParsePlugin, JoinRequestPlugin, JoinRequestPartPlugin)
without dropping unknown own properties — after the literal, any extra
string or symbol keys on the original request are copied over, so
plugin authors (e.g. webpack's ResolverCachePlugin) that attach custom
state to the request keep working unchanged.