Skip to content

perf: cache fieldProcessor output + memoize parseIdentifier#558

Merged
alexander-akait merged 1 commit into
mainfrom
claude/improve-performance-caching-YQ2Ze
Apr 26, 2026
Merged

perf: cache fieldProcessor output + memoize parseIdentifier#558
alexander-akait merged 1 commit into
mainfrom
claude/improve-performance-caching-YQ2Ze

Conversation

@alexander-akait

Copy link
Copy Markdown
Member

Two hot paths in exports/imports field resolution were still doing
repeated work across calls that have identical inputs:

  • fieldProcessor(request, conditionNames) cached findMatch and
    conditionalMapping individually but always re-ran directMapping,
    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 during
    exports/imports resolution (ExportsFieldPlugin, ImportsFieldPlugin,
    plus assertExportTarget / assertImportTarget inside
    directMapping). 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

@linux-foundation-easycla

linux-foundation-easycla Bot commented Apr 23, 2026

Copy link
Copy Markdown

CLA Not Signed

@changeset-bot

changeset-bot Bot commented Apr 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3727bda

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
enhanced-resolve 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

@codecov

codecov Bot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.95918% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 96.51%. Comparing base (b2fafb2) to head (3727bda).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/Resolver.js 93.75% 1 Missing ⚠️
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              
Flag Coverage Δ
integration 96.51% <97.95%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 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 Apr 23, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 98.17%

⚠️ 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
❌ 4 regressed benchmarks
✅ 133 untouched benchmarks

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

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)

Open in CodSpeed

@alexander-akait alexander-akait force-pushed the claude/improve-performance-caching-YQ2Ze branch 22 times, most recently from f7db4d3 to c78491a Compare April 24, 2026 21:47
@alexander-akait alexander-akait force-pushed the claude/improve-performance-caching-YQ2Ze branch 2 times, most recently from f538072 to 3b271c2 Compare April 25, 2026 23:35
…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
@alexander-akait alexander-akait force-pushed the claude/improve-performance-caching-YQ2Ze branch from 3b271c2 to 3727bda Compare April 25, 2026 23:53
@alexander-akait alexander-akait merged commit f50989b into main Apr 26, 2026
33 of 35 checks passed
@alexander-akait alexander-akait deleted the claude/improve-performance-caching-YQ2Ze branch April 26, 2026 14:13
alexander-akait pushed a commit that referenced this pull request May 7, 2026
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>
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.

2 participants