ts-r-lsp
Language server. Replaces tsserver and the ESLint server.
- Pre-alpha
- Works from source
ts-r-lsp gives every editor window on a workspace one shared server instead of a TypeScript process each. Each window starts a small front process, about 3 MB, that connects to one daemon per workspace. The daemon runs the type backend, watches memory, and adds lint diagnostics from ts-r-lint.
3.6× faster first errors
What switching looks like
Nothing is on npm yet. Once it is, switching looks like this: ts-r-lsp reads the config you already have, so the change is to the command, not the project.
[language-server.typescript]
- command = "typescript-language-server"
+ command = "ts-r-lsp"
args = ["--stdio"]
What works, and what doesn’t yet
Works today
- One daemon per workspace, shared by every window
- A memory budget: the backend restarts when idle above it, and open documents are replayed
- The ts-r checker or your own TypeScript as the type backend, with a per-project parity check
- Lint diagnostics and code actions: fixes, suggestions, Fix all and disable comments
- A tsserver stand-in so VS Code’s built-in TypeScript support can share the daemon
Not yet
- Misses its 500 MB target with every file of a large project open
- Typing in TSX files with React types is slower than tsserver
- 22 of 24 reference-app projects fall back to tsserver for diagnostics
- The VS Code extension isn’t packaged yet
- Windows is untested
Measured
Every run, on the same reference app as the home page, with the command and commit that recorded it.
ts-r-lsp with our checker against tsserver and TypeScript 7's LSP
Under 500 MB on these projects; large ones not measured
A scripted LSP client on three public projects: date-fns (1,641 sources), RxJS (231) and tRPC (154). ts-r-lsp uses far less memory than tsserver and TypeScript 7's native LSP, with every source open too, but it is slower than TypeScript 7's LSP to first diagnostics on all three and per keystroke on two of them (it re-checks each edit on a fresh checker). It also reports a few diagnostics tsc does not on the opened RxJS and tRPC files. These are medium projects: the 500 MB target is for large ones, which have not been measured.
- ts-r
- Other tools
- ts-r
- Other tools
- ts-r
- Other tools
- ts-r
- Other tools
| Measure | ts-r-lsp | tsserver | TypeScript 7 LSP |
|---|---|---|---|
| date-fns format/index.ts: first diagnostics | 416 ms | timed out | 168 ms |
| date-fns: keystroke to diagnostics p50 / p95 | 14.1 / 14.7 ms | timed out | 5.0 / 5.8 ms |
| date-fns: memory, one file / all 1,641 open | 164 / 232 MB | timed out | 469 / 710 MB |
| RxJS merge-map.ts: first diagnostics | 155 ms | 953 ms | 108 ms |
| RxJS: keystroke to diagnostics p50 / p95 | 12.2 / 12.8 ms | 28.6 / 35.1 ms | 13.3 / 17.2 ms |
| RxJS: memory, one file / all 231 open | 138 / 377 MB | 840 / 1,091 MB | 397 / 820 MB |
| tRPC router.ts: memory, one file / all 154 open | 95 / 165 MB | 571 / 757 MB | 477 / 480 MB |
| tRPC: hover p50 / p95 | 0.08 / 3.77 ms | 0.75 / 9.66 ms | 0.32 / 5.68 ms |
| tRPC: completion p50 / p95 | 0.08 / 0.25 ms | 0.38 / 12.53 ms | 0.22 / 0.40 ms |
| Diagnostics on the opened file (date-fns / RxJS / tRPC) | 0 / 2 / 3 | – / 0 / 0 | 0 / 0 / 2 |
How we measured
- Measured
- A scripted LSP client: first diagnostics, hover, definition and completion latency (20 sampled positions), keystroke to diagnostics (30 keys, 150 ms apart) on one file, then memory with that file open and with every source of the project open (5 s settle); one run per tool
- Machine
- Apple M-series MacBook (macOS arm64, 16 cores, 48 GB), shared with other jobs while measuring; release build, 4 threads
- Software
- ts-r-lsp release build with the ts-r backend and the tsc parity check off; each repo's own tsserver; TypeScript 7.0.2's native LSP (tsc --lsp) pinned in bench/package.json
- Input
- date-fns, RxJS and tRPC at the commits pinned in bench/repos.toml; the files and source directories are each repo's [repo.editor] set
- Command
cargo xtask bench --fetch-repos --repos date-fns,rxjs,trpc && cargo xtask bench --editor --repos date-fns,rxjs,trpc (prints the commands; the editor-latency.js ones were run)- Recorded in
- commit
9c3a371
Lint in the editor: ts-r-lsp against the ESLint extension's server
Time from a keystroke to new lint diagnostics, and server memory, in tRPC's largest server source (22 KB, resolveResponse.ts) with the repo's own ESLint config and all 154 sources open. ts-r runs the rules it implements: 35 of the config's rules are not run in the editor (17 not implemented yet, 17 from plugins, 1 type-aware), so the ESLint server does more work.
- ts-r
- Other tools
- ts-r
- Other tools
How we measured
- Measured
- A scripted LSP client opens every source, then types 300 keystrokes 100 ms apart into a comment and times didChange to diagnostics; RSS of the server at the end; one run each
- Machine
- Apple M-series MacBook (macOS arm64, 16 cores, 48 GB), shared with other jobs while measuring; release build, 4 threads
- Software
- ts-r-lsp release build; the ESLint VS Code extension's server 3.0.34 run headlessly with pull diagnostics; the repo's own ESLint
- Input
- tRPC at the commit pinned in bench/repos.toml: packages/server/src/unstable-core-do-not-import/http/resolveResponse.ts, with packages/server/src and packages/client/src open
- Command
scripts/limit.sh -- node editors/vscode/bench/lint-keystrokes.js ts-r target/release/ts-r-lsp target/bench-repos/trpc <file> --open-all --src-dirs packages/server/src,packages/client/src (eslint mode with eslintServer.js for the other)- Recorded in
- commit
9c3a371
ts-r-lsp: two editor windows on your own TypeScript
ts-r-lsp can also run your project's own TypeScript behind one shared server per workspace, instead of our checker. This measures total memory for two editor windows on tRPC in that mode (two files open in each, one window in packages/server and one in packages/client). The shared server holds both windows' projects, so it is larger than one plain tsserver but smaller than two.
- ts-r
- Other tools
Time to first diagnostics in the same run: first window 1,252 ms (plain tsserver 856 ms), second window 5 ms (plain tsserver 548 ms).
How we measured
- Measured
- Total resident memory (RSS) of every process in each setup, summed with their children, after both windows have diagnostics for every open file; one run
- Machine
- Apple M-series MacBook (macOS arm64, 16 cores, 48 GB), shared with other jobs while measuring; release build, 4 threads
- Software
- TypeScript 6.0.3 (tRPC's tsserver) as the type backend
- Input
- tRPC at the commit pinned in bench/repos.toml; window A opens packages/server/src/unstable-core-do-not-import/router.ts and packages/server/src/index.ts, window B packages/client/src/index.ts and packages/client/src/links/httpBatchLink.ts
- Command
cargo build --release -p ts_r_lsp --example two_windows && scripts/limit.sh -- target/release/examples/two_windows target/bench-repos/trpc- Recorded in
- commit
9c3a371
Command line
An excerpt of ts-r-lsp --help. You can also run it through the launcher as ts-r lsp.
Next to tsserver
Editors other than VS Code start ts-r-lsp --stdio as an ordinary language server. When a project opens, the daemon runs your own tsc in the background and compares results; where they differ, diagnostics come from tsserver while hover, definition and the rest stay on ts-r.
ts-r-lsp: one shared, memory-budgeted language server per workspace
ts-r-lsp [--stdio] [OPTIONS] LSP server on stdio
ts-r-lsp tsserver [ARGS] tsserver stand-in on stdio
ts-r-lsp status [--root DIR] print the daemon's status as JSON
ts-r-lsp stop [--root DIR] ask the daemon to exit