perf: enable incremental TypeScript reparsing via getChangeRange#2961
Conversation
Previously, both SvelteDocumentSnapshot and JSOrTSDocumentSnapshot returned undefined from getChangeRange(), forcing TypeScript to do a full reparse on every snapshot update. This adds a computeChangeRange() function that scans for the minimal diff between old and new text, enabling TS's incremental parser. Benchmarks show 2-5x faster reparsing for structural edits and near-instant reparsing for token-level edits (e.g. typing/renaming). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 8e6ecbb The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds tests using realistic svelte2tsx output (~150 lines with script, store subscriptions, template, and component export) and a full TypeScript service class (~200 lines with decorators, generics, and multiple methods). Tests cover common edit patterns on these files and verify that incremental TS reparsing produces correct ASTs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
I think we could add the change. However, the difference isn't really that big since it's not the part that spends the most time. I tried running a benchmark with our update process, including svelte2tsx, parseHTML and ts program update. The difference is around 5-10% average in 100 iterations. If starting from two 1000-line files and updating both per iteration, it went from 1300ms to 1200ms. So it might not be noticeable in most cases. For the code changes, I don't think we need the "incremental reparse AST verification" part. We can keep a few of them. Most of the test AI generated in |
- Remove incremental-reparse-benchmark.ts (served its purpose) - Trim DocumentSnapshot.test.ts from 152 to 31 tests, keeping only essential coverage: basic operations, edge cases, reconstruction invariant, snapshot integration, and in-place update tests - Remove redundant/over-tested sections Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Thank you for the reply! I do not really have the full grasp of all of the inner workings of this setup (i tried to do some refactorings earlier as well), so i figured it would be worth it to just take a couple of shots with my best AI setup and see if something good comes out of it. I implemented your feedback, want to re-review? |
Also, could you share from your benchmarking what part spends the most time? Or is this a benchmark that is already in the repo that i can run? |
|
You can find it here https://github.com/jasonlyu123/language-tools/tree/incremental-benchmark While setting up this branch, I remembered we actually tried this before in #1192. |
packages/language-server/src/plugins/typescript/DocumentSnapshot.ts
Outdated
Show resolved
Hide resolved
packages/language-server/test/plugins/typescript/DocumentSnapshot.test.ts
Show resolved
Hide resolved
- JSOrTSDocumentSnapshot.update() returns new snapshot instead of mutating in-place - SvelteDocumentSnapshot.getChangeRange returns undefined on scriptKind/parserError changes - Trim tests to essential coverage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@jasonlyu123 do you want to review the updates |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Previously, both
SvelteDocumentSnapshotandJSOrTSDocumentSnapshotreturnedundefinedfromgetChangeRange(), which meant TypeScript had to fully reparse every file on each snapshot update. This PR implementsgetChangeRange()properly, enabling TypeScript's built-in incremental parser.What changed
computeChangeRange()function — a simple linear scan (usingcharCodeAtfor speed) that finds the minimal diff between old and new text, returning ats.TextChangeRangewith the exact changed span.SvelteDocumentSnapshot.getChangeRange()— now computes the change range between the old snapshot's text and the current text instead of returningundefined.JSOrTSDocumentSnapshot.getChangeRange()— same as above, with an additional optimization: when TS passes the same object reference (which happens becauseupdate()mutates the snapshot in-place and TS cached the reference before mutation), it returns a pre-computed change range from the lastupdate()call, avoiding a redundant diff.computeChangeRangecorrectness andgetChangeRangeintegration for both snapshot types.How
computeChangeRangeworksWhy this matters
getChangeRange()is called by TypeScript every time a source file snapshot changes. Previously returningundefinedforced a full reparse of the entire file. With a proper change range, TypeScript's incremental parser can:In end-to-end benchmarks (including svelte2tsx, parseHTML, and TS program update), this yields roughly a 5-10% improvement in the full update pipeline.
Test plan
npm testinpackages/language-server)🤖 Generated with Claude Code