perf: cache internal TS ASTs with incremental reparsing#2963
perf: cache internal TS ASTs with incremental reparsing#2963MathiasWP wants to merge 3 commits intosveltejs:masterfrom
Conversation
Use ts.updateSourceFile() instead of ts.createSourceFile() for the internal script ASTs inside svelte2tsx. When the same cache object is passed across invocations for a file, only the changed subtree is reparsed, yielding ~27% faster svelte2tsx transforms on typical single-character edits (~0.3ms saved per keystroke for a 100-line component). The cache is keyed per-file in the language server and falls back to full parsing when no prior AST is available or content is unchanged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: b1cc663 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
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 |
jasonlyu123
left a comment
There was a problem hiding this comment.
This will increase memory usage. In my testing, the memory usage is around 5% increase, but it might be a bigger problem in a bigger project. I think it'll be better cache only the files that are opened in the editor (Document.openedByClient). We can save some unnecessary memory allocation cost this way. You also need to remove the cache when the file is closed or deleted. The logic is most suitable in LSAndTSDocResolver.
In svelte-check without the --watch flag. The cache doesn't do anything, so there should be a way to skip it.
I actually also tried this before, but the difference is so small that it is sometimes slower. We'll need a more end-to-end benchmark to check if it's actually faster.
| /** | ||
| * Optional cache for reusing internal TypeScript ASTs across invocations. | ||
| * Pass the same object for repeated calls on the same file to enable | ||
| * incremental TS reparsing (~2x faster script parsing). |
There was a problem hiding this comment.
| * incremental TS reparsing (~2x faster script parsing). | |
| * incremental TS reparsing. |
We shouldn't add this claim here. There are too many variables that affect the number. It doesn't make sense to add it as a documentation.
| } | ||
|
|
||
| /** Per-file cache for reusing internal TypeScript ASTs across svelte2tsx invocations */ | ||
| const svelte2tsxCacheMap = new Map<string, Svelte2TsxCache>(); |
There was a problem hiding this comment.
This file should not have any top-level state. You can put it as a field for GlobalSnapshotsManager. This also needs to be a FileMap for path case-sensitivity.
There was a problem hiding this comment.
Or a field in Document, so we don't need a map, and it'll be cleared when the document is removed.
|
I've noticed that benchmarks i write with Claude doesn't seem to reflect real improvements when in an E2E environment. I asked you earlier about how you benchmarked this, but i'm wondering if it would be possible to create an E2E script where i could run benchmarks to get proper output? It seems like i just end up with incorrect results, which in turn makes the optimisations useless. I really want to help, so if you have an idea on what would be required to create a script that could do a proper E2E test, i could try to set this up? @jasonlyu123 |
|
To be clear, I was mostly referring to the "improvement ratio". If the improvement target accounts for only a minor portion of the overall time, it might not be significant, even if it's 50x faster. In that case, the "50x faster" claim is just kind of unhelpful. You can reference |
| * Pass the same object for repeated calls on the same file to enable | ||
| * incremental TS reparsing (~2x faster script parsing). | ||
| */ | ||
| cache?: Svelte2TsxCache; |
There was a problem hiding this comment.
I think we don't need a concrete type here. It can be a Record<string, unknown>. It's a map for svelte2tsx to store the cache. The consumer of the API doesn't need to know what is in the cache.
Summary
ts.SourceFileASTs acrosssvelte2tsxinvocations and usets.updateSourceFile()for incremental reparsing instead ofts.createSourceFile()on every callsvelte2tsx, enabling TypeScript to reuse unchanged AST subtreesBenchmark results
Isolated svelte2tsx (80 sequential single-char edits, realistic ~100 line component):
Full LSP round-trip (type char → hover):
How it works
Inside
svelte2tsx, there are twots.createSourceFile()calls that parse<script>and<script context="module">contents into TypeScript ASTs. These account for ~18% of totalsvelte2tsxtime.TypeScript provides
ts.updateSourceFile(oldAst, newText, changeRange)which reuses unchanged AST subtrees. For single-character edits (typical typing), only the affected statement/block is reparsed.The implementation:
Svelte2TsxCacheinterface with fields for cached ASTs and their contentcomputeScriptChangeRange()to computets.TextChangeRangeby scanning common prefix/suffixgetOrUpdateSourceFile()that usests.updateSourceFile()when a cached AST exists, falls back tots.createSourceFile()otherwiseTest plan
ts.updateSourceFile()preserves parent node references🤖 Generated with Claude Code