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.

tRPC router.ts: first diagnosticsShorter is faster. Pre-release build, real app.
  • ts-r-lsp224 ms
  • tsserver800 ms

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.

languages.toml (Helix)
  [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.

RxJS: memory with all 231 sources openShorter is faster, in MB
  • ts-r
  • Other tools
  • ts-r-lsp377 MB
  • TypeScript 7 LSP (tsc --lsp)820 MB
  • tsserver (the repo's TypeScript)1,091 MB
date-fns: memory with all 1,641 sources openShorter is faster, in MB
  • ts-r
  • Other tools
  • ts-r-lsp232 MB
  • TypeScript 7 LSP (tsc --lsp)710 MB
tRPC router.ts: keystroke to diagnostics, p50Shorter is faster, in ms
  • ts-r
  • Other tools
  • TypeScript 7 LSP6.2 ms
  • ts-r-lsp9.4 ms
  • tsserver24.3 ms
tRPC router.ts: first diagnosticsShorter is faster, in ms
  • ts-r
  • Other tools
  • TypeScript 7 LSP58 ms
  • ts-r-lsp224 ms
  • tsserver800 ms
Per project
Measurets-r-lsptsserverTypeScript 7 LSP
date-fns format/index.ts: first diagnostics416 mstimed out168 ms
date-fns: keystroke to diagnostics p50 / p9514.1 / 14.7 mstimed out5.0 / 5.8 ms
date-fns: memory, one file / all 1,641 open164 / 232 MBtimed out469 / 710 MB
RxJS merge-map.ts: first diagnostics155 ms953 ms108 ms
RxJS: keystroke to diagnostics p50 / p9512.2 / 12.8 ms28.6 / 35.1 ms13.3 / 17.2 ms
RxJS: memory, one file / all 231 open138 / 377 MB840 / 1,091 MB397 / 820 MB
tRPC router.ts: memory, one file / all 154 open95 / 165 MB571 / 757 MB477 / 480 MB
tRPC: hover p50 / p950.08 / 3.77 ms0.75 / 9.66 ms0.32 / 5.68 ms
tRPC: completion p50 / p950.08 / 0.25 ms0.38 / 12.53 ms0.22 / 0.40 ms
Diagnostics on the opened file (date-fns / RxJS / tRPC)0 / 2 / 3– / 0 / 00 / 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.

Keystroke to lint diagnostics, p50Shorter is faster, in ms
  • ts-r
  • Other tools
  • ts-r-lsp --lint-only15.6 ms
  • ESLint server 3.0.3453.3 ms
Server memory, all 154 sources openShorter is faster, in MB
  • ts-r
  • Other tools
  • ts-r-lsp --lint-only10.6 MB
  • ESLint server 3.0.342,296 MB
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.

Memory for two editor windowsShorter is faster, in MB
  • ts-r
  • Other tools
  • ts-r-lsp, one shared server698 MB
  • Two plain tsservers838 MB
  • VS Code default setup1,451 MB

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 --help
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