perf: keep the typescript check's program between rebuilds - #332
Merged
Merged
Conversation
Every compilation built a program from nothing and asked it for every diagnostic the project has, so a watch rebuild paid for the whole project however little had changed. The program is kept for the compiler that built it and handed to the one after it, as a semantic-diagnostics builder over a host that answers with the source file it read last time for as long as the file on disk is untouched — which is what lets TypeScript reuse its work and check only what the change reaches. Over three hundred files in a real watch build: rebuilds of 240-360 ms become 145 ms for the first and about 30 ms after, for the 49 MB the program holds on to. A change to the config file or to the compiler options given here starts a new program, since nothing the last one was built from survives it. What is reported is what `getPreEmitDiagnostics` asks a program for, asked of the builder instead so that a file it has already checked is not checked again. Claude-Session: https://claude.ai/code/session_01GzZci4NQeiqwdrVfd7dGXy Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Every compilation built a program from nothing and asked it for every diagnostic the project has, so a watch rebuild paid for the whole project however little had changed. The program is now kept for the compiler that built it and handed to the one after it — a semantic-diagnostics builder over a host that answers with the source file it read last time for as long as the file on disk is untouched, which is what lets TypeScript reuse its work and check only what the change reaches.
Measured in a real webpack watch build over three hundred TypeScript files, three runs each:
It costs the memory the program holds on to: 108.6 MB retained against 59.4 MB, measured after three rebuilds with
--expose-gc. That is the same tradefork-ts-checker-webpack-pluginandvite-plugin-checkermake, and it is only paid while watching — a one-shot build releases the program with the compiler.A change to the config file, or to the compiler options given here, starts a new program: nothing the last one was built from survives it. What is reported is what
getPreEmitDiagnosticsasks a program for, asked of the builder instead so that a file it has already checked is not checked again.What kind of change does this PR introduce?
perf
Did you add tests for your changes?
Yes — a case in
test/typescript/watch.test.jsfor the thing incremental checking can get wrong: a file that is itself fine, whose changed exports break a different file, and which must be reported against the dependent. It also asserts the second program was handed the first, through a recorder wrapping the real TypeScript (test/typescript/mock/typescript-recorder). Before writing any of this I checked the incremental result against a fullgetPreEmitDiagnosticsover eleven mutations — a comment, a type break, a shape change breaking a dependent, a fix, a deletion, a new bad file, andstrictoff and back on — and they agreed at every step.Does this PR introduce a breaking change?
No. The set of diagnostics reported is unchanged.
If relevant, what needs to be documented once your changes are merged or what have you already documented?
Documented in this PR: the TypeScript section said the program was rebuilt whenever a checked file changed, which is no longer true, and now says what a rebuild costs and what starts a new program.
Use of AI
Claude Code wrote this change, guided and reviewed by me. Both the speedup and the memory it costs were measured rather than asserted, and the incremental result was checked against a full one before the mechanism was ported.
🤖 Generated with Claude Code
https://claude.ai/code/session_01GzZci4NQeiqwdrVfd7dGXy
Generated by Claude Code