Skip to content

perf: keep what the TypeScript check found between runs - #383

Merged
alexander-akait merged 2 commits into
mainfrom
perf/ts-persist-builder-state
Oct 23, 2026
Merged

alexander-akait merged 2 commits into
mainfrom
perf/ts-persist-builder-state

Conversation

@alexander-akait

Copy link
Copy Markdown
Member

Part of the performance series (#376–#382).

Problem

In watch mode the TypeScript check keeps its builder program warm. But every new process starts from nothing: a one-shot build, a CI run, a restarted dev server. In the default readonly mode the check never wrote any state, and no mode ever read a .tsbuildinfo back, so each run type-checked the whole program again.

The survey for this series found rollup's TypeScript plugins and tsc --incremental avoiding exactly this, with readBuilderProgram and emitBuildInfo.

Change

Applies when the program writes nothing (readonly, the default) and cache isn't false:

  • Where the state goes: a .tsbuildinfo under the check's cacheLocation. The new default is node_modules/.cache/diagnostics-webpack-plugin/typescript/, with one file per config file and set of compiler options. A cacheLocation ending in .tsbuildinfo is used as the file itself.
  • Building: with incremental and that tsBuildInfoFile, using the EmitAndSemantic builder (only it has emitBuildInfo). The state is written after each check.
  • Reading back: a compiler's first program reads the state with ts.readBuilderProgram. If it's unreadable or from another TypeScript version, the check starts from nothing.
  • File versions: a hash of each file's content instead of mtime:size, so a state written by another process still matches after a checkout or a CI cache restore. The stat-based reuse of parsed files in watch mode is unchanged.
  • Unchanged: the modes that write keep using the config file's own incremental, and so does build.

Measured

301 files, TypeScript check (no skipLibCheck), two runs each:

wall CPU peak RSS
one-shot, main (every run) 4.7–4.9 s 12.2–12.6 s 560–575 MB
one-shot, this PR, first run 4.7 s 12.1 s 580 MB
one-shot, this PR, later runs 1.96–2.04 s 5.9–6.2 s 456–466 MB
watch, this PR, restart with state: first build / rebuild 1 1.95 s / 150 ms
watch, main: first build / rebuild 1 4.0–4.2 s / 520–610 ms

webpack alone takes about 1.6 s on this project, so on a later run the check adds about 0.4 s. Watch rebuilds after the first are unchanged.

Tests

  • New test/typescript/state.test.js:
    • a second run starts from the state the first one wrote;
    • an edit between runs that breaks a file which didn't itself change is reported;
    • cache: false writes nothing and reads nothing.
    • Mutation-checked: giving every file the same version makes the second test fail.
  • Test setup: the TypeScript test config now gives each compiler its own temporary cacheLocation, as the ESLint tests already do. The recorder mock records the EmitAndSemantic builder too.
  • Full suite: 317/317, global module path cleared, as on CI. eslint, tsc, prettier, cspell and lint:schema are clean.
  • README (TypeScript, cacheLocation) and a minor changeset are updated, and types regenerated.

Generated by Claude Code

A one-shot build, CI run or restarted dev server type checked the whole
program from nothing: nothing the last run found was kept, and
`readBuilderProgram` was never called.

A program that writes nothing (the default `readonly` mode) is now built
with `incremental` and a `tsBuildInfoFile` in the check's cache, by the
EmitAndSemantic builder, whose state `emitBuildInfo` writes after each
check. A compiler's first program reads it back. Files are versioned by
a hash of what they hold rather than by time and size, so a state
written by another process, after a checkout, is told from the files as
they are. A mode that writes is left as it was; `cache: false` keeps
nothing.

301 files, one-shot: 4.7 s / 12.2 s CPU / 560 MB -> 2.0 s / 6.0 s CPU /
460 MB on the second run. Watch rebuilds unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01App18sYbe6Vf6HDCjCXbvf
@linux-foundation-easycla

linux-foundation-easycla Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

CLA Not Signed

One or more co-authors of this pull request were not found. You must specify co-authors in commit message trailer via:

Co-authored-by: name <email>

Supported Co-authored-by: formats include:

  1. Anything <id+login@users.noreply.github.com> - it will locate your GitHub user by id part.
  2. Anything <login@users.noreply.github.com> - it will locate your GitHub user by login part.
  3. Anything <public-email> - it will locate your GitHub user by public-email part. Note that this email must be made public on Github.
  4. Anything <other-email> - it will locate your GitHub user by other-email part but only if that email was used before for any other CLA as a main commit author.
  5. login <any-valid-email> - it will locate your GitHub user by login part, note that login part must be at least 3 characters long.

Alternatively, if the co-author should not be included, remove the Co-authored-by: line from the commit message.

Please update your commit message(s) by doing git commit --amend and then git push [--force] and then request re-running CLA check via commenting on this pull request:

/easycla

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 16 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ 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: f7c51aa8-6520-4390-b177-8d3e07c5a73d
📥 Commits

Reviewing files that changed from the base of the PR and between f62bdb5 and ef4efed.

📒 Files selected for processing: 8
  • .changeset/ts-keep-state-between-runs.md
  • README.md
  • src/checks/typescript-program.js
  • src/checks/typescript.js
  • test/typescript/mock/typescript-recorder/index.js
  • test/typescript/state.test.js
  • test/typescript/utils/conf.js
  • types/checks/typescript.d.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Copy link
Copy Markdown
Member Author

EasyCLA fails here for the same reason as on #366–#375: the commit's Co-Authored-By: Claude trailer doesn't match a GitHub user with a signed CLA. The code isn't involved. Clearing it means a maintainer override or removing the trailer, so I'm leaving it for the maintainers to decide.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

One CI job failed: Test - windows-latest - Node v26.x, Webpack latest, ESLint latest. The failing test is test/incremental.test.js › "should lint again what a change reaches among the files it globs" (3 files linted, 2 expected). It is not this PR's:

  • The test drives the ESLint check through a mocked ESLint with a watch rebuild.
  • This PR only touches the TypeScript check (typescript.js, typescript-program.js), and the test passed on every other OS/Node/ESLint combination of the same commit.

The likely cause is a late watcher event on Windows: a rebuild picks up the fixture's utimes reset on watch-other.js. No fix exists yet. I'm re-running the failed job once. If it fails again, I'll make that test ignore watcher events for files it did not edit, the same way #379's watch test does.

Separately, EasyCLA fails because the commits carry a Co-Authored-By: Claude trailer that maps to no GitHub account. That is for the maintainers to decide on; I haven't rewritten history.


Generated by Claude Code

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

codecov Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.64%. Comparing base (f62bdb5) to head (ef4efed).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff            @@
##             main     #383    +/-   ##
========================================
  Coverage   99.64%   99.64%            
========================================
  Files          19       19            
  Lines        5570     5689   +119     
========================================
+ Hits         5550     5669   +119     
  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.

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