Skip to content

Conversation

@MathiasWP
Copy link

@MathiasWP MathiasWP commented Dec 6, 2025

I'm on a journey to improve the performance the performance of the (amazing) svelte language tools in VSCode, since i'm working a large Svelte project that definitely feels the pain, and so i wanted to start with code that i believe is used quite often and can easily be improved.

Initially i wanted to actually debug hot-code usage in the vscode extension, but still after reading https://github.com/sveltejs/language-tools/blob/master/CONTRIBUTING.md I am struggling to understand how i can get output like console.logs in the debugger of the vscode extension - any help here is appreciated! So the methods i chose to update here are based on "how hot i think they are" and "easy performance improvements".

Now back to the PR. I wanted to change these three methods:

  • getTextInRange
  • getLineAtPosition
  • isInsideMoustacheTag

But before doing so, i wanted to ensure that the file had more tests, and while i added the tests for that util file, i decided to add some more tests to the other utils file in case i want to change that one in the future.

In getTextInRange and getLineAtPosition each offsetAt call with only text will compute getLineOffsets(text) again (because of the default parameter), so:

  • getTextInRange → 2 × getLineOffsets(text)
  • getLineAtPosition → 2 × getLineOffsets(text)

That’s O(n) over the whole text each time, instead of once. So i refactored the code to compute line offsets once.

In isInsideMoustacheTag the html.substring usage allocates a brand new string for every call, so the code is refactored to use String.prototype.lastIndexOf with a fromIndex instead, which scans without allocations

@changeset-bot
Copy link

changeset-bot bot commented Dec 6, 2025

🦋 Changeset detected

Latest commit: 09ace34

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
svelte-language-server Patch

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
Copy link
Member

jasonlyu123 commented Dec 7, 2025

About getTextInRange and getLineAtPosition. These two functions were often called with Document and DocumentSnapshots. Both should already have a cache of lineOffsets. It should be possible to go further to make it an instance method to utilise the cache.

One of the reasons isInsideMoustacheTag is using substring is also for performance. lastIndexOf could be expensive in components with a large amount of HTML. To avoid using substring, maybe you could try looping through the index and use charCodeAt to check for moustache. Also, the string that is passed into isInsideMoustacheTag is also a substring, so you could probably check that as well.

For debugging the language server, you also need to use the Attach debugger to language server action in the vscode debugger. If you want to perform performance analysis, you can follow the steps:

  1. Start debugging the extension. Or set the "svelte.language-server.port": config to 9222 and restart the editor.
  2. Open Chrome/Edge, go to chrome://inspect. Wait a bit. There should be a svelte language server, and you can inspect it.
  3. After you click the inspect link, a dev tool will open. You can check the memory snapshot or the CPU profile.

You might want to locate the performance problem using the dev tool first. Some of the bigger problems we know are because of libraries causing poor type-check performance. In that case, it might be better to switch the library in your project or wait for the typescript-go (But it might make some libraries get away with more extreme stuff LOL)

@MathiasWP
Copy link
Author

About getTextInRange and getLineAtPosition. These two functions were often called with Document and DocumentSnapshots. Both should already have a cache of lineOffsets. It should be possible to go further to make it an instance method to utilise the cache.

One of the reasons isInsideMoustacheTag is using substring is also for performance. lastIndexOf could be expensive in components with a large amount of HTML. To avoid using substring, maybe you could try looping through the index and use charCodeAt to check for moustache. Also, the string that is passed into isInsideMoustacheTag is also a substring, so you could probably check that as well.

For debugging the language server, you also need to use the Attach debugger to language server action in the vscode debugger. If you want to perform performance analysis, you can follow the steps:

  1. Start debugging the extension. Or set the "svelte.language-server.port": config to 9222 and restart the editor.
  2. Open Chrome/Edge, go to chrome://inspect. Wait a bit. There should be a svelte language server, and you can inspect it.
  3. After you click the inspect link, a dev tool will open. You can check the memory snapshot or the CPU profile.

You might want to locate the performance problem using the dev tool first. Some of the bigger problems we know are because of libraries causing poor type-check performance. In that case, it might be better to switch the library in your project or wait for the typescript-go (But it might make some libraries get away with more extreme stuff LOL)

Thank you for the informative answer @jasonlyu123! This makes sense, and i definitely have to become more familiar with the codebase so i don't do unnecessary changes like caching cached stuff 😄

Being able to debug is definitely the best approach, thanks for giving me more info on how to do this! The reason i'm trying to find performance improvements in the svelte language server is because intellisense is instant in our typescript files, but they're noticeable slower in svelte files. Sometimes so slow that AI is actually faster with autocompletion than waiting for the intellisense to finish loading.

I will try debugging more and see if i can find bottlenecks that cause the long loading!

@MathiasWP
Copy link
Author

@MathiasWP
Copy link
Author

MathiasWP commented Dec 7, 2025

I have managed to find something interesting. The updateIfDirty call in lsContainer.getService seems to be a bottleneck on the "initial load" for intellisense in Svelte files. It almost always takes around 200ms to execute, but i have seen it some times go up to 400-700ms. This definitely makes it feel less "snappy":

Screen.Recording.2025-12-07.at.15.16.29.mov

The updateIfDirty seems to get called every time an intellisense window is opened, so improving this would definitely help!

I also debugged that lang.getCompletionsAtPosition can sometimes take almost up to a second, but it usually hovers on 40-140ms most of the time:

image

Note that updateIfDirty is very consistently slow at around 200ms, while lang.getCompletionsAtPosition sometimes can become much slower, which in turn creates the annoying moments where you wait a really long time.

@jasonlyu123
Copy link
Member

jasonlyu123 commented Dec 7, 2025

When the language server starts, TypeScript will create a "program" object that contains type information about the project. The updateIfDirty function is mostly a wrapper around ts.LanguageService.getProgram to ensure that some state is updated correctly during the TypeScript program update. The initial program creation is always slower. But the program won't be rebuilt until you make a change to one of the files. After the edit, TypeScript will try to reuse stuff and create a new program.

The first getCompletionsAtPosition is also slower because it needs to process auto-import. But it will be cached, so the calls afterwards should be faster. In TypeScript's TSServer, this is done in the first program creation to make it one initial loading.

If you could find some hot paths in our code, that would be easier to fix. Otherwise, it'll be hard to figure out if it's a TypeScript problem. Or if it's caused by an inefficiency of our LanguageServiceHost implementation.

@MathiasWP
Copy link
Author

MathiasWP commented Dec 7, 2025

When the language server starts, TypeScript will create a "program" object that contains type information about the project. The updateIfDirty function is mostly a wrapper around ts.LanguageService.getProgram to ensure that some state is updated correctly during the TypeScript program update. The initial program creation is always slower. But the program won't be rebuilt until you make a change to one of the files. After the edit, TypeScript will try to reuse stuff and create a new program.

This doesn't seem to be the case in my debugging. For me, the updateIfDirty method runs around 3-5 times on every keystroke (unless i'm typing really fast), and the first one is always around 200ms in duration. There is something in the wrapping here that makes it noticeable slower, because it opens immediately in TypeScript files.

If you want we could do some debugging together on frontend project i work at. It is around 200k lines of Svelte and TypeScript (ca 50/50) - so quite a good test for larger projects.

@jasonlyu123
Copy link
Member

It's normal for updateIfDirty to run multiple times on every keystroke. It'll update trigger TypeScript to update the program if the project state is dirty and needs an update. But 200ms indeed is a bit slow.

Could you record a CPU profile following the steps I mentioned before? If you need to discuss this privately or if you want to provide the project. You can DM me on Discord. You can find me in the Svelte Discord.

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