# tsrs vs tsgo: Cloudflare Said 8×—So Why Does the Benchmark Say 16.87×?

> Canonical: https://vxi.me/en/posts/tsrs-vs-tsgo-typescript-rust-compiler/
> Locale: en-us
> Published: 2026-10-22
> Updated: 2026-10-22
> Markdown: https://vxi.me/en/posts/tsrs-vs-tsgo-typescript-rust-compiler.md

<figure class="tsrs-parity-hero not-prose">
<picture><source media="(prefers-reduced-motion: reduce) and (max-width: 640px)" srcset="/images/posts/tsrs-vs-tsgo-typescript-rust-compiler/tsrs-diagnostic-parity.mobile.still.en-us.svg" type="image/svg+xml"><source media="(prefers-reduced-motion: reduce)" srcset="/images/posts/tsrs-vs-tsgo-typescript-rust-compiler/tsrs-diagnostic-parity.still.en-us.svg" type="image/svg+xml"><source media="(max-width: 640px)" srcset="/images/posts/tsrs-vs-tsgo-typescript-rust-compiler/tsrs-diagnostic-parity.mobile.en-us.svg" type="image/svg+xml"><img src="/images/posts/tsrs-vs-tsgo-typescript-rust-compiler/tsrs-diagnostic-parity.en-us.svg" alt="tsgo and tsrs run four TypeScript synthetic fixtures against a matching TypeScript source revision; diagnostics and exit codes match in all four." width="1200" height="700" loading="eager" fetchpriority="high" decoding="async" style="display:block;width:100%;height:auto"></picture>
<figcaption>Figure: Diagnostic comparison on four single-file synthetic fixtures. All four outputs match; this is not a project compatibility or performance benchmark.</figcaption>
</figure>

Cloudflare introduced tsrs with an eye-catching claim: up to 8× faster and roughly 70% less memory than TypeScript 7.0. Then I opened an older benchmark report pinned to a tsrs commit. There it was: 16.87× for mui-docs and 13.55× for vscode under the defaults. My first reaction was simple: how do those numbers fit with the 8× headline?

I did not rerun the large-project performance benchmarks. Instead, I took two compilers built against the corresponding TypeScript source revision, put them in a temporary isolated GitHub-hosted environment, and compared their diagnostics on four small TypeScript fixtures. All four matched. That is enough to make the next experiment worth doing—not enough to replace the type checker in production CI.

## Why the checker count matters when reading the benchmark

Cloudflare's [announcement](https://blog.cloudflare.com/typescript-rust-port/) summarizes its early comparison as up to 8× faster with roughly 70% lower memory. The pinned tsrs [historical benchmark report](https://github.com/oxc-project/tsrs/blob/357d1d3ec23180eae94a8485b8b7444487a7ea5f/bench/RESULTS.md) gives the following per-project results:

| Historical upstream case | tsgo | tsrs | Checker count | Reported speedup |
| --- | ---: | ---: | --- | ---: |
| mui-docs, defaults | 12.66 s | 0.75 s | 4 vs 16 | 16.87× |
| mui-docs, both `--checkers 4` | 12.69 s | 1.07 s | 4 vs 4 | 11.81× |
| mui-docs, both `--checkers 16` | 12.36 s | 0.77 s | 16 vs 16 | 16.07× |
| vscode, defaults | 11.10 s | 0.82 s | 4 vs 16 | 13.55× |
| vscode, both `--checkers 16` | 6.73 s | 0.81 s | 16 vs 16 | 8.32× |

The defaults aren't even using the same worker count: tsgo gets four checkers, while tsrs gets sixteen. Yet even the rows with matching counts report more than 8×. The important distinction is that the announcement is an early headline, while `RESULTS.md` is a separate historical snapshot with its own pinned versions and environment. Eight times is not a ceiling for that table; sixteen times is not a promise for your project.

Why is the gap so large? I cannot infer a single cause just by staring at the rows. What the report actually documents is a Depot CI `depot-ubuntu-24.04-16` machine (16 vCPU, 63 GB RAM, Linux x86_64), with interleaved runs and medians for elapsed time and peak memory. The tsgo baseline uses `typescript@7.0.2`; tsrs ports a newer TypeScript 7.1-dev commit and uses a PGO-optimized release build. Matching checker counts does not make the source revisions and build setups identical.

Memory changes with the project and settings too. For mui-docs, the report gives a 4.61× lower-memory ratio with defaults and 8.23× when both sides use sixteen checkers. The announcement's “roughly 70% less” is a summary of early measurements, not a per-project guarantee. I checked the upstream report for these numbers; I did not reproduce its large-project benchmark.

## What the four diagnostic fixtures actually tested

I pinned `tsrs@0.18.0-ts7.1.0-dev.20260929` against the Go implementation distributed as `typescript@7.1.0-dev.20260930.4`. Both correspond to the same [TypeScript source commit, b85298b6](https://github.com/microsoft/TypeScript/commit/b85298b6a81f772d080b0455de0ca9d744cd6fd6).

I hit a very specific setup mistake first: I tried installing `@typescript/typescript`, a workspace package name used by the TypeScript source repository, as if it were published on npm. npm returned E404; the compiler never started. The publicly published root package is `typescript`. At this pinned nightly version the CLI is still called `tsc`, and the Linux x64 native Go compiler is provided by `@typescript/typescript-linux-x64`. Fixing the package name got me to the actual compilation step.

All four fixtures shared one `tsconfig.json`. I explicitly set `types: []` so ambient global `@types` packages from the environment wouldn't silently enter the test, and `skipLibCheck: false` to keep declaration-file checking enabled. Here is the actual configuration, rather than just calling it “strict mode”:

~~~json
{
  "compilerOptions": {
    "strict": true,
    "target": "es2022",
    "module": "esnext",
    "noEmit": true,
    "skipLibCheck": false,
    "types": []
  },
  "include": ["src/*.ts"]
}
~~~

Each fixture directory contains just one `src/test.ts`:

| Fixture directory | Complete `src/test.ts` | Observed tsgo / tsrs result |
| --- | --- | --- |
| `valid` | `export const answer: number = 42;` | 0 diagnostics, exits 0 / 0 |
| `type-error` | `export const answer: number = "wrong";` | TS2322, exits 2 / 2 |
| `module-missing` | `import {missing} from "./unavailable.js"; export const answer = missing;` | TS2307, exits 2 / 2 |
| `generic-constraint` | `type OnlyText<T extends string> = {value:T}; export type Bad = OnlyText<number>;` | TS2344, exits 2 / 2 |

Both compilers were invoked with `--noEmit --pretty false --checkers 4`. I compared file paths, line and column positions, error codes, messages, diagnostic order, and process exit codes. Everything matched across these four fixtures. But a single source file per fixture does not meaningfully exercise four-way parallelism; the checker flag here mostly fixes an explicit test parameter.

## Equal error counts don't necessarily mean equal diagnostics

Seeing the same number of errors on both sides can be misleading. In its pinned [compatibility notes](https://github.com/oxc-project/tsrs/blob/357d1d3ec23180eae94a8485b8b7444487a7ea5f/docs/COMPATIBILITY.md), tsrs reports matching 13,458 out of 13,462 error baselines, plus all 12,779 `.types` and `.symbols` baselines.

The upstream `drizzle-orm` result shows why the details matter. With sixteen checkers on each side, the report says both tools found 10,846 errors, but their diagnostic locations differed, and labels this a tsrs bug. I did not reproduce that case myself; I'm citing the pinned report. In CI or a code review, matching totals are little comfort if an error points to the wrong file or line.

The compatibility table also lists practical limitations: this version does not implement `--init` or `--showConfig`; `--locale` does not translate messages; and the `.astro`, `.vue`, and `.svelte` content mappers work for CLI/build but not yet in the language server. Prebuilt packages do not cover Windows or Intel macOS either. My isolated Linux run exercised the CLI, not those machines, editor plugins, or the LSP. If your day-to-day workflow depends on `astro check` or live editor diagnostics, check the [version-specific compatibility table](https://github.com/oxc-project/tsrs/blob/357d1d3ec23180eae94a8485b8b7444487a7ea5f/docs/COMPATIBILITY.md) before experimenting. I also have an [Astro and TypeScript 7 compatibility note](/posts/astro-check-typescript-7-compatibility/) if you're troubleshooting an upgrade.

## What I'd compare before trying it in my own project

Start in a throwaway worktree or separate branch, not your everyday working directory. The point is to make it easy to discard the experiment without disturbing your project.

This script catches a subtle mistake: both programs may exit with the same code even though the actual diagnostics differ. `set +e` is intentional. A deliberately broken fixture exits nonzero; we need to let both compilers finish, capture their status, and only then compare output. `tsc` is the Go compiler's CLI in this pinned TypeScript nightly, while `/tmp/tsgo.out` is just the log filename.

~~~bash
pnpm add -D --save-exact tsrs@0.18.0-ts7.1.0-dev.20260929 typescript@7.1.0-dev.20260930.4

set +e
pnpm exec tsc -p . --noEmit --pretty false --checkers 4 > /tmp/tsgo.out 2>&1
tsgo_status=$?
pnpm exec tsrs -p . --noEmit --pretty false --checkers 4 > /tmp/tsrs.out 2>&1
tsrs_status=$?
diff -u /tmp/tsgo.out /tmp/tsrs.out
diff_status=$?
set -e
printf 'tsgo_exit=%s tsrs_exit=%s diff_exit=%s\n' "$tsgo_status" "$tsrs_status" "$diff_status"
if [ "$tsgo_status" -ne "$tsrs_status" ] || [ "$diff_status" -ne 0 ]; then
  echo 'DIAGNOSTIC_DIFF_OR_EXIT_MISMATCH: inspect /tmp/tsgo.out and /tmp/tsrs.out'
  exit 1
fi
~~~

If the first comparison differs, preserve the raw outputs. Then try `--checkerAssignment go` (tsgo-style file assignment and checker history) or `--noLazyMembers` (disabling the tsrs lazy-member optimization) as separate troubleshooting experiments. Don't enable them before the first comparison, or you lose sight of default-behavior differences. The details are in the [usage guide](https://github.com/oxc-project/tsrs/blob/357d1d3ec23180eae94a8485b8b7444487a7ea5f/docs/USAGE.md) and [default-differences reference](https://github.com/oxc-project/tsrs/blob/357d1d3ec23180eae94a8485b8b7444487a7ea5f/docs/DEFAULTS.md).

The diagnostic script does not measure elapsed time or memory. I didn't run a full performance comparison this time. For a first look at your own project, GNU `time` on Linux can record one run's elapsed time and peak RSS. On macOS, install `gnu-time` and change the first line to `TIME_BIN=gtime`.

~~~bash
TIME_BIN=/usr/bin/time

set +e
"$TIME_BIN" -f 'elapsed_s=%e max_rss=%M' -o /tmp/tsgo-bench.metrics \
  pnpm exec tsc -p . --noEmit --pretty false --checkers 4 \
  > /tmp/tsgo-bench.out 2>&1
tsgo_status=$?
"$TIME_BIN" -f 'elapsed_s=%e max_rss=%M' -o /tmp/tsrs-bench.metrics \
  pnpm exec tsrs -p . --noEmit --pretty false --checkers 4 \
  > /tmp/tsrs-bench.out 2>&1
tsrs_status=$?
set -e

printf 'tsgo_exit=%s ' "$tsgo_status"; cat /tmp/tsgo-bench.metrics
printf 'tsrs_exit=%s ' "$tsrs_status"; cat /tmp/tsrs-bench.metrics
~~~

Those two commands produce only one pair of observations; background load can easily skew that result. For a meaningful comparison I'd alternate the run order for at least five paired rounds on the same runner, pin dependencies, compiler versions, `tsconfig`, and checker count, and compare medians and peak RSS. Cold starts and warm-cache runs need separate labels. I did not clear the operating system's page cache, so I wouldn't describe these as fully cold system-cache measurements. GNU `time` usually reports `%M` as peak RSS in KiB; check units before comparing platforms.

I'd first put tsrs into CI as an additional, non-blocking job while keeping the existing `tsc` and framework checks as required gates. Only after unexplained diagnostic differences are resolved, measurements stabilize, the job survives a real PR/release cycle, and rollback is clear would I consider replacing anything. Four tiny fixtures establish one useful fact: for these inputs, the two compilers produced the same diagnostics.

Experiment date: October 22, 2026
