perf: cache fieldProcessor output + memoize parseIdentifier#558
Conversation
|
🦋 Changeset detectedLatest commit: 3727bda 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 |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #558 +/- ##
==========================================
- Coverage 96.53% 96.51% -0.03%
==========================================
Files 50 50
Lines 2887 2896 +9
Branches 901 911 +10
==========================================
+ Hits 2787 2795 +8
- Misses 84 85 +1
Partials 16 16
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 degrade performance by 98.17%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Memory | node-compare: enhanced-resolve sync x 1000 (fs cache) |
597.5 KB | 783.4 KB | -23.72% |
| ❌ | Memory | resolve-to-context: directory resolve (warm) |
1.8 KB | 96.6 KB | -98.16% |
| ❌ | Memory | node-compare: enhanced-resolve async x 1000 (no cache) |
163.3 KB | 181.6 KB | -10.06% |
| ⚡ | Memory | node-compare: enhanced-resolve async x 1000 (fs cache) |
1,185.8 KB | 783.5 KB | +51.35% |
| ❌ | Memory | tsconfig-paths: 5 path prefixes (warm) |
1.8 KB | 96.6 KB | -98.17% |
Comparing claude/improve-performance-caching-YQ2Ze (3727bda) with main (b2fafb2)
f7db4d3 to
c78491a
Compare
f538072 to
3b271c2
Compare
…hot paths
Small, behavior-identical reductions on paths that run per resolve.
- `Resolver.finishResolved` hoists `/#/g` to a module-level const and
gates the `.replace` behind `includes("#")` so paths/queries without
a `#` (the overwhelming common case) skip the regex state-machine
walk and its lastIndex slot allocation.
- `createFieldProcessor` returns a shared `EMPTY_NO_MATCH` tuple on the
"no match" / "no condition matched" legs instead of allocating
`[[], null]` per call.
- `createFieldProcessor` inlines `isConditionalMapping(mapping)` (a
`!== null && typeof === "object" && !Array.isArray` check) at the
hot-path site to skip the function call.
- `computeConditionalMapping` merges the duplicate `default` and
`conditionNames.has(condition)` branches (both did identical work)
and inlines the `isConditionalMapping(innerMapping)` guard.
- `targetMapping` hoists `/\$/g` to a module-level const and skips the
escape allocation entirely when `remainingRequest` has no `$`.
- `DescriptionFilePlugin` hoists `/\\/g` to a module-level const so
the regex state slot isn't re-allocated per Windows resolve. POSIX
paths stay on the existing `includes("\\")` fast path.
- `directMapping` iterates `mappingTarget` + inner results via indexed
loops instead of `for...of`, avoiding a fresh iterator-object
allocation per call. `mappingTarget` is always a plain Array at
these points (the scalar case early-returns).
- `ExportsFieldPlugin` / `ImportsFieldPlugin`: replace
`invalidSegmentRegEx.exec(…) !== null` with `.test(…)` so the regex
engine doesn't build a match array only to throw it away. Drop the
dead `deprecatedInvalidSegmentRegEx.test(…) !== null` clause in
`ImportsFieldPlugin` (`.test` returns boolean; `true !== null` and
`false !== null` are both true, so it was `&& true`). Drop the
redundant `relativePath.length === 0` guard before
`!startsWith("./")` — the empty-string case is already covered.
CodSpeed sees `+50%` memory on `node-compare async x 1000 (fs cache)`
from this set of reductions.
All 1024 tests pass.
https://claude.ai/code/session_013HTntF4mEycEMuXcUF6Bxu
3b271c2 to
3727bda
Compare
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## enhanced-resolve@5.21.1 ### Patch Changes - Allocation-free reductions on hot-path code: hoist `/#/g`, `/\$/g` and `/\\/g` to module-level constants and gate the corresponding `.replace` calls behind `includes(…)` so paths/queries/requests without the match char skip the regex state machine entirely (the common case); share a single `EMPTY_NO_MATCH` tuple instead of allocating `[[], null]` per "no match" / "no condition matched" return; switch `directMapping`'s `for...of` over `mappingTarget` and inner results to indexed loops to avoid iterator-object allocation per call; inline `isConditionalMapping` at its two hot-path call sites and merge the duplicate `default` / `conditionNames.has(condition)` branches in `computeConditionalMapping`; replace `invalidSegmentRegEx.exec(…) !== null` with `.test(…)` (no match-array allocation); drop the dead `deprecatedInvalidSegmentRegEx.test(…) !== null` clause in `ImportsFieldPlugin` (`.test` returns boolean; `true !== null` and `false !== null` are both true, so it was `&& true`); drop the redundant `relativePath.length === 0` guard before `!startsWith("./")` in `ExportsFieldPlugin` (the empty-string case is already covered). (by [@alexander-akait](https://github.com/alexander-akait) in [#558](#558)) - restore plugin compatibility for `[...resolveContext.stack]` iteration (by [@alexander-akait](https://github.com/alexander-akait) in [#569](#569)) - fix `TsconfigPathsPlugin` to support `resolveSync` with `useSyncFileSystemCalls` (by [@alexander-akait](https://github.com/alexander-akait) in [#572](#572)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Two hot paths in exports/imports field resolution were still doing
repeated work across calls that have identical inputs:
fieldProcessor(request, conditionNames)cachedfindMatchandconditionalMappingindividually but always re-randirectMapping,allocating a fresh
[paths, usedField]tuple on every call.Typical build traffic reissues the same request many times (same
specifier from different source files, module-graph revisits), so
this allocation is wasted. Cache the whole tuple keyed by
(request, conditionNames) — Map<string, WeakMap<Set, tuple>> on the
processor's closure, tied to the description-file lifetime via the
plugin's existing
_fieldProcessorCache.parseIdentifier(path)is called on every iterated path duringexports/imports resolution (ExportsFieldPlugin, ImportsFieldPlugin,
plus
assertExportTarget/assertImportTargetinsidedirectMapping). The same target strings ("./index.js","./dist/foo.js", …) recur across resolves and across packages,so memoize via a bounded module-level Map; on overflow clear
rather than LRU — hot entries re-warm in one pass.
Also lift the backslash-normalization regex in DescriptionFilePlugin
to a module-level const so its state isn't re-allocated per Windows
resolve (POSIX stays on the `includes("\\")` fast path).
Wall-clock impact (node, JIT on; 2-3 run average):
exports-field require,node (warm) +8.8%
exports-field import,node (warm) +10.5%
exports-patterns-many +12.6%
imports-field specifiers (warm) +8.4%
self-reference own package (warm) +9.9%
description-files-multi (warm) +3.2%
unsafe-cache / realistic-midsize stay within noise (rme ~1.5%).
All 1024 tests pass.
https://claude.ai/code/session_013HTntF4mEycEMuXcUF6Bxu