Typecheckers are the part of my toolchain I complain about the least, mostly because I have trained myself to run them once and walk away. That habit got harder to defend yesterday.
On October 20, Cloudflare and VoidZero announced tsrs, a Rust port of the TypeScript compiler, built by Max Schwenk at Owner.com and now living in the Oxc project. It is not a fork: they are working with the TypeScript team, the Go compiler remains the reference implementation, and the goal is a stable release with full conformance against TypeScript 7.1. It installs with npm install tsrs, and the repo calls itself experimental.
The headline is that it checks up to 8x faster and uses around 70% less memory than TypeScript 7.0 at the same number of type check workers. Both numbers are real, both come from the vendor, and only one of them explains why the project exists.
📉 The Number That Broke Was Not Seconds
Owner’s backend monolith, Olympus, is about 37,000 TypeScript files typechecked as a single Program. On TypeScript 7.0, one single-threaded check needed 19 GiB of memory and 80 seconds of wall time. Run tsgolint next to it and you spin up a second checker, which doubles the memory. Add language and dev servers and you are north of 50 GiB for what Schwenk fairly calls basic validation.
The interesting number there is not 80 seconds. Eighty seconds is annoying, but it is cacheable, shardable, and invisible if your CI runner waits for it. The 19 GiB is what changes behavior. Put that checker next to a linter, an editor, and a worktree, multiply by however many agents a developer is running, and you have a laptop that swaps itself to death. Owner says engineers on a 36 GiB MacBook could not run several agents in parallel without risking the machine locking up.
That is a capacity problem, not a latency problem, and the unit that broke was not “a build”, it was “one agent per developer”. The mitigations they tried are the ones every large TypeScript codebase reaches for: cloud sandboxes, asking agents in steering files to take turns, splitting the tsconfig into fast and slow variants, breaking the monolith into packages. Each is organizational debt dressed as a performance fix. They report typechecks at roughly seven seconds in CI, local development viable again on a normal laptop, and projected savings in the hundreds of thousands of dollars a year.
🤖 A Port That Refused to Make Design Decisions
Schwenk says he wrote zero code. Not “barely any”. Zero. He pointed Claude Opus 5.5 at the Go implementation with a profiler and the TypeScript performance wiki, ran more than 20 subagents in parallel, and woke up to a working typechecker.
The rules he set are worth stealing: same algorithms, same function structure, same evaluation order, same diagnostics, all pinned to one upstream commit, with redesign explicitly out of scope. One Rust crate per Go package, with signatures and contracts written first and implementations mocked out, so agents could work in separate worktrees without waiting on each other. And tests as the spec, using the 15,197 conformance cases the JavaScript to Go port left behind.
The first overnight run matched 78% of TypeScript’s error baselines byte for byte, and another 21% missed only because there was no type printer yet. With a printer, the match rate went to 99.97%, all but four of 13,462. The compatibility page now states it directly: 13,458 of 13,462 error baselines match, and all 12,779 .types and .symbols baselines match.
That scope discipline is the real story. They skipped JavaScript emit and a language server on day one, because Owner’s apps transpile with swc and most of the team had stopped living in editors. Both came later, and the compatibility table still shows the seam: emit is complete, the language server passes 4,074 of 4,546 fourslash groups, and framework content mappers work on the CLI but not in the language server.
🎯 Conformance Against a Compiler That Isn’t Deterministic
The compatibility document defines the target with unusual strictness: the Go code is the spec, and any difference from it in diagnostics, their order, message text, or exit code is a tsrs bug, even when the Rust output looks better. That is the right call. Your CI diff, your editor squiggles, and any test asserting on message strings all assume one order.
But the reference implementation has races in it. The repo’s README says the pinned Go compiler can produce different diagnostics and traces depending on the order of parallel parsing, resolution, and checking, and that the Go parser and resolver races can still vary between runs. So the spec they are matching byte for byte is not a fixed function. It is a function with scheduling jitter, and tsrs handles it by canonicalizing some checker history by default, then offering --checkerAssignment go --noLazyMembers when you want the closest possible imitation of the Go compiler’s ordering.
That is a real design decision smuggled in under the word “faithful”, and I think it is the correct one. A compiler whose output changes because eight threads finished in a different order is a compiler you cannot write a golden test against. The compatibility table even scores it: same output regardless of thread count, yes for tsrs with known exceptions, and no for the Go compiler.
⚙️ What the Benchmarks Say, and What They Leave Out
The 8x is a maximum, not a median. In the repo’s published table, seventeen projects on a 16 vCPU machine, tsrs beats the Go compiler by 4.37x to 11.81x at four checker threads each, with peak memory 2.19x to 7.24x lower. Run both at their defaults and tsrs takes up to one checker per core, so the spread widens to 4.20x to 16.87x faster and 2.17x to 4.61x less memory. On VS Code specifically, 7.49 GiB became 1.80 GiB at four checkers.
Two disclosures matter more than the ratios. The bench CI is disabled, so those numbers are a snapshot from a pinned commit rather than a live claim. The top-repo conformance run, which diffs diagnostics against pinned Go on 400 popular repositories, also has its automatic runs disabled. I would rather see that than a chart that never stops being published.
Error parity is the honest part. At four checkers, tsrs and the Go compiler report the same errors on 9 of 17 projects. Most mismatches are version drift, since tsrs ports a 7.1 development commit while the comparison binary is 7.0.2, and a nightly built from that commit reproduces tsrs’s errors exactly. But the 16 checker run on Drizzle ORM is a real bug: same count, 10,846, different locations. It is listed by name, which is why I trust the rest.
The comparison I would actually make is against bun check, not against Go. On VS Code at 64 threads the repo’s numbers show tsrs about 1.9x faster than Bun’s checker but using 7 to 25% more memory, so the memory advantage is over Microsoft’s Go compiler, not over the fastest thing in the field. There are three credible native TypeScript checkers now, and the fair question from Hacker News this morning stands: is tsc actually unbearably slow? For a 30 file project, no. That is not this workload.
🛠️ How I Would Adopt It
Not by swapping the compiler everyone depends on. Run one job in parallel against your existing check. It is nearly free and answers the only question that matters: do the two disagree on your code? Compare diagnostic sets, not counts, and pin both to the same TypeScript commit.
Keep it out of the editor. --init and --showConfig are unsupported, --locale is ignored so messages are English only, and there is no Windows package through the 0.18 line and no Intel macOS binary. Watch mode is Linux and macOS only. None of that hurts a CI check, and all of it hurts a laptop.
What would change my habits is the footprint. A checker peaking at 1 to 2 GiB on projects where the Go compiler needed 6 to 8 is one you can run per worktree, per agent, per sandbox. It also makes a full toolchain viable inside Wasm, which is Cloudflare’s motive: a Go binary drags its garbage collector into the guest, and Rust does not.
🧠 Final Thoughts
The 8x is a benchmark result and the 70% is a product decision. Owner did not have a speed problem, they had a memory problem that made parallel agents unsafe, and you cannot fix a garbage collector’s object graph from the outside. Rust was not chosen because it is faster on paper. It was chosen because the memory layout is yours to decide.
The risk is maintenance, and it is not subtle. Their plan is the Vinext process: an agent reads upstream diffs every morning, opens tracking issues, and a nightly job regenerates a compatibility matrix against the TypeScript suite. That is a serious answer to who keeps a port in sync, but it bets a harness absorbs years of language work faster than divergence accumulates. The evidence so far is a compatibility page tracking one pinned commit, four known baseline failures, one named location bug, and a benchmark harness that is switched off. Good enough to try in CI. Not yet good enough to hand my editor.
If your checks are already fast and your machines are not on fire, wait for the stable release. If your CI runners are the reason your agents take turns, run it beside your current check this week and count the disagreements.
Thanks for reading.