Skip to content

perf: sign typed-lint results once per build rather than once per batch - #397

Merged
alexander-akait merged 2 commits into
mainfrom
perf/typescript-check
Oct 30, 2026
Merged

alexander-akait merged 2 commits into
mainfrom
perf/typescript-check

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Refs #374.

While webpack builds, the ESLint check gets files in batches of about 80. Under typed linting each batch called signatures() from scratch: it parsed every tsconfig.json again and stat-ed every file of the program again. So one build paid for the whole project once per batch.

The fix

createSigner(graph) returns a signer that one ESLint check instance keeps for its build, which is one compilation (src/check.js:207). Within a build it:

  • stats each file and parses each config file once,
  • computes the project signature once,
  • keeps the signatures of import cycles (strongly connected components) it has already worked out for later batches.

If a config file turns out to have changed, it starts over, as before. signatures(graph, files) stays as a one-batch wrapper.

Measured inside a warm build of the barrel-shaped project from #395 (3,000 modules):

signatures(), main:  38 batches × ~6,000 stat calls, 1.9 s of CPU
signatures(), here:  0.3 s of CPU
warm build, typed ESLint (parserOptions.project) + use: 'typescript'
                                     threads: false          threads on (default)
eslint-webpack-plugin + fork-ts      10.7–11.1 s, 965 MiB    n/a
main                                 13.3–13.7 s, 1,325 MiB  10.3–11.2 s, 850 MiB
this PR                              11.9–12.4 s, 1,320 MiB  11.0–11.2 s, 865 MiB

Memory is the peak memory of the whole process tree, including fork-ts-checker's own process.

What's left with threads: false

About 1.2 s and 350 MiB still separate threads: false from the old plugins. That cost is the TypeScript check itself: 824 ms in createProgram and 310 ms building the incremental state, run on webpack's thread and held in webpack's heap. fork-ts-checker pays roughly the same, but in a separate process alongside the build. That's what threads: false asks for, and the README already says so. With threads on (the default), the plugin matches or beats the old plugins.

Tests

  • test/import-graph.test.js adds two tests.
    • One checks that later batches of a build don't read tsconfig.json again. Per-batch signing, as on main, reads it 3 times for the first batch and 7 times after three; the signer stays at 3.
    • The other checks that signing in batches gives the same signatures as signing all files at once.
  • Locally, npm run lint passes and the full suite (369 tests) passes, with NODE_PATH unset as before.


Generated by Claude Code

Summary by CodeRabbit

  • Performance
    • Typed-linting files are now read once per build rather than once per batch, reducing repeated work.
    • Configuration and dependency information is reused across batches when possible, while changes to configuration are detected and reflected in results.
    • Signing results remain consistent whether files are processed together or across separate batches.

While webpack builds, the ESLint check is handed files in batches, and
every batch signed its files from scratch: it parsed each config file
again and checked every file of the program again, so a build paid for
the whole project once per batch. One signer now answers for a build,
reading each file and config file once and keeping the signatures it
worked out for the batches after.

Over 3,000 modules checked by typed ESLint and the TypeScript check with
threads: false, a warm build took 13.3-13.7 s and now takes 11.9-12.4 s;
signing took 1.9 s of it and now takes 0.3 s. Refs #374.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013FhutQipYi25fJ4rs9WE1c
@codecov

codecov Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.69%. Comparing base (8153e2a) to head (31efd31).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #397   +/-   ##
=======================================
  Coverage   99.69%   99.69%           
=======================================
  Files          21       21           
  Lines        6552     6630   +78     
=======================================
+ Hits         6532     6610   +78     
  Misses         20       20           

☔ View full report in Codecov by Harness.
📢 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.

@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: da8e0611-2554-4c1a-a90e-bac973fa9ede
Commits

Reviewing files that changed from the base of the PR and between 99fdaaf and 31efd31.

Files selected for processing: 2
  • src/import-graph.js
  • test/import-graph.test.js

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

Walkthrough

The import graph now provides a reusable signer that caches file and config reads across batches. It recomputes signatures when previously recorded config hashes change. The ESLint check retains the signer for remembered results. Tests cover config reads across batches and compare batched signatures with signatures from a single batch. A patch changeset records the release note.

Priority: Low

Merge Risk: Low · up to 31efd

The change reduces signing work per build. In rare cases an autofix within one build could let a dependent file reuse a stored lint result based on the old contents, and the next build corrects it. This is acceptable to merge with owner awareness.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title says the change signs typed-lint results once per build instead of once per batch. The raw summary and objectives both describe a build-scoped signer (createSigner) that reuses signature wor…
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat…
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.
Full details: Title check

Explanation

The title says the change signs typed-lint results once per build instead of once per batch. The raw summary and objectives both describe a build-scoped signer (createSigner) that reuses signature work across ESLint batches, so the title matches the main change. The title is 71 characters, which is within a reasonable length for a commit-style title, and it is specific enough for a teammate scanning history to understand the primary change.

Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 2 functions across 3 files.

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

test/import-graph.test.js

Oops! Something went wrong! :(

ESLint: 10.10.0

SyntaxError: Unexpected token ':'
at compileSourceTextModule (node:internal/modules/esm/utils:319:16)
at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:89:18)
at #translate (node:internal/modules/esm/loader:438:20)
at afterLoad (node:internal/modules/esm/loader:506:29)
at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:511:12)
at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:564:36)
at afterResolve (node:internal/modules/esm/loader:611:52)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:617:12)
at node:internal/modules/esm/loader:636:32
at TracingChannel.tracePromise (node:diagnostics_channel:370:14)

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.

@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: 1


Review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 822ff5b9-a3a0-425e-befa-00a87d660916
Commits

Reviewing files that changed from the base of the PR and between 8153e2a and 99fdaaf.

Files selected for processing: 4
  • .changeset/sign-once-per-build.md
  • src/checks/eslint.js
  • src/import-graph.js
  • test/import-graph.test.js

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

Comment thread src/import-graph.js Resolved
…g file

A config file first reached in a later batch had no hash recorded and was
taken for one that changed: the signer forgot everything the build had
read and read the batch again. In a monorepo with a tsconfig.json per
package that happened for every batch reaching a new package. Only a
config file whose recorded hash differs now starts over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013FhutQipYi25fJ4rs9WE1c
@alexander-akait
alexander-akait merged commit 36731b5 into main Oct 30, 2026
17 checks passed
@alexander-akait
alexander-akait deleted the perf/typescript-check branch October 30, 2026 13:02
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.

1 participant