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.
12× 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
Misses the 500 MB target
The editor target is at most 500 MB with every file of a large project open. ts-r-lsp misses it: 587 MB with all 649 private-app sources open, about the same as TypeScript 7's native LSP. Typing in a React TSX file is also slower than tsserver today, because every edit re-checks on a fresh checker. On this project 22 of 24 tsconfig projects still fall back to tsserver for diagnostics, because our checker does not match tsc on them yet.
- ts-r
- Other tools
- ts-r
- Other tools
- ts-r
- Other tools
- ts-r
- Other tools
| Measure | ts-r-lsp | tsserver 6.0.3 | TypeScript 7 LSP |
|---|---|---|---|
| Largest file: keystroke to diagnostics p50 / p95 | 4.3 / 5.1 ms | 10.5 / 16.1 ms | 4.4 / 6.9 ms |
| Largest file: hover p50 / p95 | 0.21 / 0.45 ms | 0.42 / 3.49 ms | 0.14 / 0.70 ms |
| Largest file: definition p50 / p95 | 0.27 / 0.52 ms | 0.22 / 0.65 ms | 0.10 / 0.20 ms |
| Memory with the largest file open | 43 MB | 442 MB | 108 MB |
| card-form.tsx: first diagnostics | 44 ms | 270 ms | 27 ms |
| card-form.tsx: completion p95 | 1.19 ms | 7.35 ms | 2.69 ms |
How we measured
- Measured
- A scripted LSP client: first diagnostics, hover, definition and completion latency, keystroke to diagnostics (100 keys, 150 ms apart) on one file, and memory with one file or all 649 open (24 projects, 15 s settle)
- Machine
- Apple M-series Mac, macOS arm64, release build (the exact chip was not recorded)
- Software
- ts-r-lsp release build with the ts-r backend and the tsc parity check off; tsserver 6.0.3 (the project's TypeScript); TypeScript 7.0.2's native LSP (tsc --lsp)
- Input
- private-app; the largest file is packages/api-client/src/schema.gen.ts, the TSX file apps/web/.../card-form.tsx
- Command
scripts/limit.sh -- node editors/vscode/bench/editor-latency.js ts-r target/release/ts-r-lsp <root> <file> (tsserver and tsgo modes for the others)- Recorded in
- commit
59268bf
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 the largest linted private-app file (20 KB) with the project's own configs. The ESLint server also runs 41 type-aware rules that ts-r skips.
- ts-r
- Other tools
- ts-r
- Other tools
How we measured
- Measured
- A scripted LSP client types 300 keystrokes 100 ms apart into a comment and times didChange to publishDiagnostics; RSS of the server
- Machine
- Apple M-series Mac, macOS arm64, release build (the exact chip was not recorded)
- Software
- ts-r-lsp release build; the ESLint VS Code extension's server 3.0.34 run headlessly with pull diagnostics
- Input
- private-app domains/listings/src/application/profiles.test.ts (20 KB), project configs
- Command
scripts/limit.sh -- node editors/vscode/bench/lint-keystrokes.js ts-r target/release/ts-r-lsp <root> <file> (eslint mode with eslintServer.js for the other)- Recorded in
- commit
9555833
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 the same project in that mode.
- ts-r
- Other tools
Time to first diagnostics in the same run: cold window about 420 ms (plain tsserver about 370 ms), second window about 140 ms (plain tsserver about 330 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
- Machine
- Apple M-series Mac, macOS arm64, release build (the exact chip was not recorded)
- Software
- TypeScript 6.0.3 (tsserver) as the type backend
- Input
- private-app, two windows with 2 files open in each
- Command
cargo build --release -p ts_r_lsp && scripts/limit.sh -- cargo run --release -p ts_r_lsp --example two_windows -- /path/to/repo- Recorded in
- commit
bf06dc4
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