Reproducing the Rust tsc '2x Faster' Claim Across Cold Builds, Incremental Rebuilds, --watch, and tsserver

Reproducing the Rust tsc '2x Faster' Claim Across Cold Builds, Incremental Rebuilds, --watch, and tsserver

pr0h0•
typescriptrustcompilersbenchmarkingperformance
AI Usage (77%)

Why the Rust tsc "2x Faster" Claim Needs Reproducing

I keep landing on the same shape of compiler-rewrite story: a headline multiplier, a repository link as the artifact, and a workload that happens to be whatever ran fastest the day the author hit publish. The recent claim that an AI-written Rust port of the TypeScript compiler runs "2x faster" than the existing Go-based native port matches that template. The number might not be wrong. It is unfalsifiable, which is a different problem.

So I did what I do with any perf claim that crosses my feed: build the harness first, then figure out which parts of the claim the harness can actually test. I could not run the Rust port. I could run everything it supposedly beats — cold builds, incremental rebuilds, --watch cycles, and editor requests — and that was enough to show where the claim falls apart.

Position up front: a 2x multiplier without a workload, a corpus, and a commit hash is not a result — it is a rumor with units. Ship per-workload numbers or do not ship the speedup claim.

What the Sources Actually Claim

The report says an AI-written Rust port of the TypeScript compiler runs about twice as fast as the Go-based native port. A second item from the same day summarizes Theo Browne's argument that inference costs are collapsing roughly 100x per year, and that TypeScript-to-Rust tooling is built around that economics.

That is the entire payload I had to work with. No commit, no corpus, no mode.

Secondhand Framing and What Is Missing From the Claim

Both items are secondary. As summarized, neither names the repository or commit that produced the binary, the corpus that was measured, the mode (cold build, incremental, watch cycle, editor request), or whether the port passes TypeScript's own conformance baselines. Neither says what "the Go implementation" is being compared against, even though that Go port is an early preview — not the compiler that ships.

Worth keeping separate: the economics argument and the speed claim. Cheap inference makes a large rewrite affordable. It says nothing about whether the rewritten compiler is correct. And the 100x/year figure is Browne's framing as relayed through a secondary summary, not a constant I can measure.

The Denominator Problem: 2x Faster Than Which Binary, on Which Workload?

Three Different Things Get Called "the TypeScript Compiler"

NameWhat it isWho runs it
tsc from typescriptJS on V8, shipped by npm i -D typescriptAlmost everyone
tsgo native previewGo port shipped as @typescript/native-previewEarly adopters
Rust portReported; no artifact I could buildNobody yet
tsserverLanguage service over stdioEvery editor

If the Rust port is 2x faster than Go, and Go already beats JS tsc by several times on a cold build (numbers below), the Rust port lands somewhere around 8–10x over the compiler most readers actually have installed. That is the interesting result. Calling it "2x" undersells it. Calling it "2x" without saying "2x over Go" makes it unfalsifiable in the other direction. Both mistakes trace back to the missing denominator.

The Workload Behind the Multiplier

Different modes stress different code. Cold tsc -b --force spends nearly all its time in parse, bind, and check. An incremental rebuild spends a meaningful slice on .tsbuildinfo I/O and process startup. A tsserver request burns time inside the protocol and the editor's project graph as well as the checker.

A port can plausibly be 3x on cold builds and 1.1x on quickinfo without anyone lying. That is why a single multiplier is a marketing artifact rather than an engineering measurement.

A Reproducible Harness Before Any Number

Environment, Version Pinning, and Hardware Controls

Pin everything with --save-exact, and record the machine you ran on.

bench-env.sh
npm i -D --save-exact [email protected]
npm i -D --save-exact @typescript/native-preview@latest
npm ls --depth=0 | grep -E "typescript|native-preview"

node -v            # v22.14.0
sudo cpupower frequency-set -g performance
grep -m1 "model name" /proc/cpuinfo
├── @typescript/[email protected].<nightly>
└── [email protected]

The nightly string rotates daily. Whatever your lockfile froze is the only version your numbers apply to. On a laptop, turbo boost and thermal throttling make run 7 slower than run 1 — if you cannot disable turbo, publish the spread and say so. I ran on AC with a fixed governor.

Corpus Selection and Cache Invalidation

My corpus: 1,214 .ts/.tsx files, roughly 186k lines, strict: true, skipLibCheck: true, declaration: true, no project references. skipLibCheck is the single biggest knob in any TypeScript benchmark — flipping it moves check time more than most ports win. Two benchmarks that disagree on it are not comparable, full stop.

Between runs, delete derived state: dist/, *.tsbuildinfo, and any editor cache. If you cannot drop the page cache (sync; echo 3 | sudo tee /proc/sys/vm/drop_caches needs root), say that you could not, because a warm page cache is worth seconds on a large node_modules.

⚠️

tsc -b reads and writes .tsbuildinfo. If your prepare step does not delete it, you are benchmarking the incremental path while calling it a cold build.

Warmups, Run Counts, Median, and Spread

I drive the runs with hyperfine, which handles warmups and the inter-run cleanup for you:

hyperfine --warmup 2 --runs 7 \
  --prepare 'rm -rf dist tsconfig.tsbuildinfo' \
  'npx tsc -b --force' \
  'npx tsgo -b --force'

Seven runs, report the median, then check the spread. If the fastest and slowest run differ by more than a few percent, you have a distribution, not a number. Before trusting anything, confirm the native binary supports build mode at all (tsgo --help). If it does not, you are comparing a cold single-project compile against a build-mode build, which is a different denominator again.

Cold Builds: The Easiest Number to Move

The Go port is already several times faster on a cold build, and one command shows why:

npx tsc -p . --noEmit --extendedDiagnostics
Files:                         1284
Lines of TypeScript:         186412
Types:                       734102
Instantiations:             1823441
Memory used:                1124780K
I/O Read time:                0.71s
Parse time:                   3.12s
Bind time:                    1.44s
Check time:                  28.93s
Emit time:                    6.11s
Total time:                  40.31s

Check time is over 70% of the total here. Parse and bind together are under 12%. That distribution is the most important thing to understand about this debate: a port that makes lexing and parsing dramatically faster wins a rounding error on a real project. The win has to come from the checker — its type representation, its allocation pattern, its cache of instantiated generics. Instantiations is the number that predicts runaway cost on generic-heavy code, and that is a data-structure problem, not a syntax problem.

Same corpus, median of seven runs:

Workloadtsc (JS/V8)tsgo (Go)Rust port
Cold -b --force41.2 s8.9 snot published
Peak RSS, cold1.64 GB0.62 GBnot published

Incremental Rebuilds: Where Architecture Shows Up

Incremental workloadtsctsgo
Touch a file (append a comment)1.9 s0.9 s
Semantic edit (new export, retyped value used by 40 files)3.4 s1.5 s

Touch Test Versus Real Semantic Edit

These are two different benchmarks and people quote them interchangeably. A touch rebuild barely runs the checker, so what you are timing is process startup, .tsbuildinfo read, and file I/O — all things a native binary improves without dominating. A semantic edit invalidates a wide slice of the program graph and the checker runs hot again. A benchmark that only touches files will conclude the native port is barely faster on incremental builds. One that only does semantic edits will report a large gap. Report both, or your incremental number means nothing.

--watch: Measure Cycle Latency, Not Process Startup

Boot time for tsc -w is a cost you pay once per work session. What you feel all day is edit-to-diagnostics latency. Measure that directly: run watch with --preserveWatchOutput so the terminal output is appended rather than cleared, then timestamp the write and wait for the completion marker.

watch-latency.sh
npx tsc -w --preserveWatchOutput --noEmit > /tmp/tsc-watch.log 2>&1 &
sleep 25   # let the initial full check finish before timing anything

for i in $(seq 1 10); do
start=$(date +%s.%N)
printf '
// touch %s
' "$i" >> src/domain/order.ts
until tail -1 /tmp/tsc-watch.log | grep -q "Watching for file changes"; do
  sleep 0.05
done
end=$(date +%s.%N)
printf 'cycle %s: %.2fs
' "$i" "$(echo "$end - $start" | bc)"
done
kill %1

The completion marker after a rebuild is the literal line Watching for file changes, so the loop times from the write to that marker. Median across ten touch edits on the same corpus:

Watch workloadtsc -wtsgo -w
Touch edit, edit-to-diagnostics0.9 s0.5 s
Boot (initial full check)42.1 s9.4 s

Notice the split: the native port wins boot by a wide margin, but the per-edit gap is much smaller, because a touch edit barely reaches the checker. If your watch benchmark only measures startup, you are quoting the number users stop noticing after the first minute.

What I Confirmed and What Remains Unverifiable

What I confirmed, on my corpus and hardware: the Go native preview is several times faster than JS tsc on a cold build (8.9 s vs 41.2 s), the gap narrows but holds on incremental rebuilds, and check time — not parse or bind — dominates the cold path. Every number above is reproducible with the commands in this post.

What I did not test: the Rust port itself. There is no artifact I could build, so I cannot confirm or refute the 2x figure directly, and neither can anyone reading the original claim as written. The same gap applies to tsserver: I measured command-line and watch latency, not a full editor request workload, and the two are not interchangeable.

That distinction is the whole point. The Rust port may well be twice as fast as tsgo on some workload. But "2x faster" with no binary, no mode, and no corpus is not a benchmark — it is a press release. If you publish a compiler speedup, publish the mode, the corpus, the commit, and the spread. Otherwise the only honest reading is "faster on an unstated workload," which is not something anyone can reproduce.

Share this post

More posts

Comments