Skip to content

Improve formatter performance with ESLint caching - #1214

Merged
JounQin merged 1 commit into
masterfrom
perf/cache-eslint-formatting
Jun 19, 2026
Merged

Improve formatter performance with ESLint caching#1214
JounQin merged 1 commit into
masterfrom
perf/cache-eslint-formatting

Conversation

@JounQin

@JounQin JounQin commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary

  • Cache ESLint instances and resolved configs with stable-hash-x to avoid repeated setup work.
  • Keep log serialization behind debug/trace checks and use pretty-format directly only when verbose logging is enabled.
  • Add a patch changeset and tests covering cache reuse, config resolver reuse, and trace logging.

Test plan

  • yarn vitest run --coverage
  • yarn eslint . --cache
  • yarn build
  • yarn format

Summary by CodeRabbit

  • Bug Fixes / Performance
    • Improved formatting performance by caching ESLint instances and resolved configurations.
    • Reduced unnecessary overhead by deferring expensive log serialization unless verbose logging is enabled.
    • Added conditional trace/debug logging around analysis and lint output.
  • Dependencies
    • Updated tooling and plugins, including TypeScript ESLint parser and Prettier-related ESLint formatter components.
  • Tests
    • Expanded coverage for ESLint/config caching and trace logging behavior.

Copilot AI review requested due to automatic review settings June 19, 2026 13:11
@changeset-bot

changeset-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6b3a21a

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

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

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds caching for ESLint instances (keyed by hashed eslintPath+options) in src/utils.ts and for resolved ESLint configs (keyed by hashed filePath+eslintPath+configOptions) in src/index.ts, both using stable-hash-x. Expensive trace/debug log serialization is gated behind new isTraceEnabled/isDebugEnabled helpers. The loglevel mock is made stateful and tests are extended to cover all caching paths.

Changes

ESLint Caching and Conditional Logging

Layer / File(s) Summary
Dependencies, changeset, and stateful logger mock
package.json, .changeset/quiet-llamas-format.md, __mocks__/loglevel-colored-level-prefix.ts
Adds stable-hash-x dependency and bumps related devDependencies; declares patch release; updates loglevel mock to be stateful with currentLevel, levels, getLevel, setLevel, normalizeLogLevel, and mockedMethods-based reset.
Logger exports and level-checking helpers
src/utils.ts
Exports logger and adds isDebugEnabled/isTraceEnabled helpers based on logger.getLevel() thresholds; updates imports to include ESLint/Linter types and hash from stable-hash-x.
ESLint instance caching
src/utils.ts
Adds eslintCache Map; getESLint now computes a deterministic cache key via hash({ eslintPath, eslintOptions }), returns cached ESLint instance when available, and stores new instances in the cache.
ESLint config caching and conditional logging
src/index.ts
Adds eslintConfigCache Map and imports; refactors getESLintConfig to compute stable cache key from filePath/eslintPath/configOptions and return cached result on hit; passes eslintConfig.cwd through getESLintApiOptions; gates analyze trace/debug logs behind isTraceEnabled/isDebugEnabled; rebuilds eslintConfig override via object spreads in createEslintFix with prettyFormat for all expensive log serialization.
Test isolation, new test coverage, and assertions
test/index.spec.ts
Imports include getESLint; test isolation clears eslintMock.ESLint calls; logLevel test removes manual setLevel mock; module resolution assertions check call counts instead of global state; new tests cover trace logging, ESLint instance caching, config resolver reuse, config caching for equivalent inputs, and caller-supplied eslint config honored on cache hit.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • prettier/prettier-eslint#1190: Refactors getESLint in src/utils.ts to async/dynamic-import, which is the same function this PR wraps with instance caching.
  • prettier/prettier-eslint#1193: Modifies createEslintFix in src/index.ts to enforce overrideConfigFile: true and handle plugins['@'] removal — the same code paths refactored here via object spreads.
  • prettier/prettier-eslint#1207: Aligns the loglevel-colored-level-prefix mock and types with the upstream loglevel API, directly overlapping with the stateful mock rework in this PR.

Suggested labels

enhancement, dependencies

🐇 I cached all the configs in a Map so bright,
No more hashing the same ESLint twice each night!
isTraceEnabled guards every log,
stable-hash-x cuts through the fog.
Hop, hop — repeated runs now feel light! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main objective of the changeset: implementing ESLint instance and configuration caching to improve formatter performance.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/cache-eslint-formatting

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codesandbox-ci

codesandbox-ci Bot commented Jun 19, 2026

Copy link
Copy Markdown

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@pkg-pr-new

pkg-pr-new Bot commented Jun 19, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/prettier-eslint@1214

commit: 6b3a21a

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves prettier-eslint runtime performance by reusing ESLint setup work across repeated format/analyze calls and by skipping expensive log serialization unless verbose logging is enabled.

Changes:

  • Add in-process caches for ESLint instances and resolved ESLint configs (keyed via stable-hash-x).
  • Gate expensive util.inspect-based log serialization behind debug/trace checks.
  • Update tests/mocks and add a patch changeset; bump a few dependencies.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/utils.ts Adds ESLint instance cache, exports shared logger + debug/trace helpers.
src/index.ts Adds ESLint config cache and guards verbose log serialization.
test/index.spec.ts Adds tests for caching behavior and trace logging; adjusts existing assertions.
__mocks__/loglevel-colored-level-prefix.ts Enhances logger mock to support getLevel/levels behavior used by new helpers.
package.json Adds stable-hash-x dependency; bumps test/dev deps.
yarn.lock Lockfile updates for new/updated dependencies.
.changeset/quiet-llamas-format.md Patch changeset describing the performance improvements.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/index.ts
Comment thread src/index.ts
Comment thread src/utils.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
src/index.ts (1)

361-364: ⚡ Quick win

Normalize filePath before hashing to improve cache reuse.

Line 363 hashes raw filePath. Equivalent references (relative vs absolute, different separators) can create separate keys for the same file and reduce cache effectiveness.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` around lines 361 - 364, The filePath variable is being hashed
directly without normalization, causing different representations of the same
path (relative vs absolute paths, different separators) to generate different
cache keys and reduce cache effectiveness. Normalize the filePath before passing
it to the hash function call that creates the cacheKey. Apply path normalization
to the filePath variable to ensure consistent path representations are used when
computing the hash with the hash function.
src/utils.ts (1)

687-701: ⚡ Quick win

Deduplicate concurrent cache misses for the same key.

If two calls hit Lines 687-701 concurrently with equivalent options, both can instantiate ESLint before either writes to eslintCache. Consider memoizing in-flight promises to fully realize the caching win under parallel formatting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils.ts` around lines 687 - 701, The current caching implementation in
the ESLint initialization logic (around the eslintCache.get(cacheKey) and
eslintCache.set calls) has a race condition where concurrent requests with
identical eslintOptions can both miss the cache and instantiate separate ESLint
instances. To fix this, introduce a separate in-flight promises cache alongside
the eslintCache. When a cache miss occurs, check if a promise for that cacheKey
is already in-flight; if so, return that promise to wait for the first
instantiation to complete. If not in-flight, store the instantiation promise in
the in-flight cache before importing and creating the new ESLint instance, then
move the result to the regular eslintCache once complete and remove it from the
in-flight cache.
test/index.spec.ts (1)

484-484: 💤 Low value

Inconsistent mock cleanup: mockReset vs mockClear.

Line 484 uses eslintMock.ESLint.mockReset() while line 292 (in beforeEach) uses mockClear(). Both clear call history, but mockReset() also removes any mock implementation and return values.

Using mockReset() here is likely unnecessary and inconsistent with the general cleanup strategy. If the test needs to fully reset the mock for a specific reason, add a comment explaining why.

♻️ Align with mockClear convention
   ).rejects.toThrow(error);
-  eslintMock.ESLint.mockReset();
+  eslintMock.ESLint.mockClear();
   expect(logger.error).toHaveBeenCalledTimes(1);
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/index.spec.ts` at line 484, The mock cleanup on line 484 for
eslintMock.ESLint uses mockReset() while the beforeEach hook on line 292 uses
mockClear(). Since mockReset() is more aggressive (clearing both call history
and implementation), align the cleanup strategy by changing mockReset() to
mockClear() on line 484 to match the convention used elsewhere. If mockReset()
is intentionally needed for a specific reason in this context, add a comment
explaining why the stronger reset is necessary.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/index.ts`:
- Line 29: The eslintConfigCache Map has no size limit and will grow
indefinitely as new entries are added during cache insertion operations in the
lines where entries are set to the cache. To fix this, implement a bounded cache
mechanism by adding a maximum size limit to eslintConfigCache (such as 100 or
200 entries depending on typical usage patterns) and add eviction logic that
removes the oldest or least recently used entry when the cache reaches its size
limit before inserting new entries. This prevents unbounded memory growth in
long-running processes while maintaining cache functionality.

In `@src/utils.ts`:
- Around line 33-34: The eslintCache Map at line 33 has no bounds and will grow
unbounded as new ESLint configurations are inserted at lines 687-701, causing
memory to accumulate in long-lived sessions. Replace the unbounded Map with a
bounded cache implementation that enforces a maximum size limit (such as LRU or
similar eviction strategy). When inserting new entries into the cache at lines
687-701, ensure the cache respects its size limit by evicting entries when
capacity is exceeded. This prevents the eslintCache from retaining heavy ESLint
instances indefinitely.

In `@test/index.spec.ts`:
- Line 292: The test isolation is broken because module-level caches
(eslintCache in src/utils.ts and eslintConfigCache in src/index.ts) are not
being cleared between tests. In src/utils.ts, export a clearCaches function that
clears the eslintCache Map. In src/index.ts, export a clearESLintConfigCache
function that clears the eslintConfigCache Map. Then in the test file's
beforeEach block (which currently only clears the eslintMock), import and call
both of these cache-clearing functions to ensure caches are reset before each
test runs.
- Around line 423-424: The test "resolves to the local eslint module" currently
only verifies that prettierMock.format and eslintMock.mock.lintText were called,
but does not actually validate that the correct ESLint module was resolved. Add
an assertion after the existing expect statements to verify that
globalThis.__PRETTIER_ESLINT_TEST_STATE__.eslintPath is defined, which will
confirm the proper ESLint module path was resolved during the test execution and
catch regressions where the wrong module is loaded.

---

Nitpick comments:
In `@src/index.ts`:
- Around line 361-364: The filePath variable is being hashed directly without
normalization, causing different representations of the same path (relative vs
absolute paths, different separators) to generate different cache keys and
reduce cache effectiveness. Normalize the filePath before passing it to the hash
function call that creates the cacheKey. Apply path normalization to the
filePath variable to ensure consistent path representations are used when
computing the hash with the hash function.

In `@src/utils.ts`:
- Around line 687-701: The current caching implementation in the ESLint
initialization logic (around the eslintCache.get(cacheKey) and eslintCache.set
calls) has a race condition where concurrent requests with identical
eslintOptions can both miss the cache and instantiate separate ESLint instances.
To fix this, introduce a separate in-flight promises cache alongside the
eslintCache. When a cache miss occurs, check if a promise for that cacheKey is
already in-flight; if so, return that promise to wait for the first
instantiation to complete. If not in-flight, store the instantiation promise in
the in-flight cache before importing and creating the new ESLint instance, then
move the result to the regular eslintCache once complete and remove it from the
in-flight cache.

In `@test/index.spec.ts`:
- Line 484: The mock cleanup on line 484 for eslintMock.ESLint uses mockReset()
while the beforeEach hook on line 292 uses mockClear(). Since mockReset() is
more aggressive (clearing both call history and implementation), align the
cleanup strategy by changing mockReset() to mockClear() on line 484 to match the
convention used elsewhere. If mockReset() is intentionally needed for a specific
reason in this context, add a comment explaining why the stronger reset is
necessary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1fdfbacf-31f5-41fc-b251-2235a7a8d7e2

📥 Commits

Reviewing files that changed from the base of the PR and between 918306b and a689079.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (6)
  • .changeset/quiet-llamas-format.md
  • __mocks__/loglevel-colored-level-prefix.ts
  • package.json
  • src/index.ts
  • src/utils.ts
  • test/index.spec.ts

Comment thread src/index.ts
Comment thread src/utils.ts
Comment thread test/index.spec.ts
Comment thread test/index.spec.ts
@JounQin
JounQin force-pushed the perf/cache-eslint-formatting branch from a689079 to 8109eed Compare June 19, 2026 13:23
@JounQin
JounQin force-pushed the perf/cache-eslint-formatting branch from 8109eed to 6b3a21a Compare June 19, 2026 13:25
@JounQin
JounQin requested a review from Copilot June 19, 2026 13:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 7 changed files in this pull request and generated 5 comments.

Comment thread src/index.ts
Comment thread src/utils.ts
Comment thread src/index.ts
Comment thread src/utils.ts
Comment thread test/index.spec.ts
@JounQin
JounQin merged commit 38cdf17 into master Jun 19, 2026
18 checks passed
@JounQin
JounQin deleted the perf/cache-eslint-formatting branch June 19, 2026 13: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.

2 participants