# A deep dive into tsrs

> Understand tsrs, the Rust port of TypeScript. Try the compiler, compare benchmarks, explore its architecture, and build a browser-based type checker.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-10-20 | Updated: 2026-10-20 | Topics: [TypeScript](https://flaviocopes.com/tags/typescript/) | Canonical: https://flaviocopes.com/tsrs/

**tsrs** is an experimental Rust port of the TypeScript compiler. It checks your TypeScript, can produce JavaScript and declaration files, and has a WebAssembly build that runs in a browser.

[Cloudflare and VoidZero announced the project](https://blog.cloudflare.com/typescript-rust-port/) on October 20, 2026. They are working with Max Schwenk of Owner.com, who created the port, in collaboration with the TypeScript team. The project is joining the Oxc toolchain.

The headline is up to **8× faster checking** and roughly **70% less memory** than TypeScript 7.0 at equal checker counts. Those are early vendor benchmarks, with results that depend on the workload.

There is more to understand here than a faster command. Which parts of TypeScript does tsrs replace? Why port a compiler that Microsoft already ported to Go? What does running it in a browser involve?

We'll work through those questions, run the native compiler, and build a small browser editor that reports real TypeScript errors locally. The guide uses the project's published documentation and experimental packages available in October 2026. The practical examples are small; the large-project performance numbers come from the project's own measurements.

## The language, compiler, and runtime

When you write TypeScript, you are dealing with several things that often get grouped together.

The **language** defines what your program means. It includes type annotations, generics, how types are inferred, and which assignments are allowed.

The **compiler** implements those rules. It loads files, understands imports, checks types, and can produce JavaScript.

The **runtime** executes the JavaScript. For a web page, that is the browser's JavaScript engine. For a server, it might be Node.js or Bun.

Here is a small example:

```ts
function greet(name: string) {
  return `Hello, ${name}`
}

console.log(greet('Flavio'))
```

The compiler uses `name: string` to check calls to `greet`. The JavaScript runtime receives a function without that annotation.

Changing the language used to implement the compiler doesn't mean changing the language you write. tsrs implements TypeScript's rules in Rust. You continue writing `.ts` files.

It also doesn't make your application's JavaScript run faster by itself. A compiler finishing sooner reduces the wait while developing and building. Your application's runtime performance is a separate measurement.

| Tool or stage | Its job | What you get |
| --- | --- | --- |
| Type checker | Check whether types agree | Errors, or a successful check |
| TypeScript emit | Transform TypeScript into JavaScript | JavaScript, optional declarations and maps |
| Bundler | Resolve and package an application's modules | Files ready to deploy |
| JavaScript runtime | Execute the program | The application's behavior |

Some tools perform several of these jobs. Keep the jobs separate when comparing them. A tool that removes annotations quickly may still leave type checking to another command.

My [TypeScript course](https://flaviocopes.com/courses/typescript/) covers the language itself. The guide to [what happens when Bun runs a TypeScript file](https://flaviocopes.com/what-happens-when-bun-runs-typescript/) follows the runtime side of this distinction.

## Where tsrs comes from

Microsoft moved its compiler from TypeScript to Go. That native implementation is the starting point for tsrs.

The [compatibility document](https://github.com/oxc-project/tsrs/blob/main/docs/COMPATIBILITY.md) describes tsrs as a function-by-function port of the Go implementation. Its reference is upstream TypeScript's behavior.

That means TypeScript's maintainers continue deciding how the language works. The tsrs team has to follow those decisions and reproduce them in Rust.

Even a small difference can matter. Suppose a dependency uses a complex conditional type. If one compiler accepts it and another rejects it, the application now has a compiler compatibility problem before its own code has changed.

Error messages matter too. Editors use their locations to underline code. Build tools inspect exit statuses. Tests may compare diagnostics or generated declaration files.

The project treats differences in diagnostics, message text, and exit codes as bugs, including differences that might look preferable on their own.

The [repository](https://github.com/oxc-project/tsrs) targets upstream development and aims for a stable release against TypeScript 7.1 with full conformance. The current package is experimental. That distinction matters when you decide whether a successful test on one project is enough to replace an existing check.

## Follow a file through the compiler

A compiler does more work than removing `: string` from a function parameter.

It starts by reading your project configuration. `include`, `exclude`, and `files` help determine the root files. Imports then pull more files into the program, including declaration files from installed packages.

Next it parses the source into a structured representation. A function declaration becomes something the compiler can inspect: its name, parameters, body, and the expressions inside it.

Binding connects names to declarations. When the checker sees a call to `greet`, it needs to find the function that name refers to.

Checking then asks whether the argument matches the parameter. With `greet(42)`, it finds a number where the function expects a string.

The illustration walks through the source, the relationships the compiler needs, the type error, and the possible outputs. Each scene adds information to the same example.

If emit is enabled, the compiler transforms the source and prints the output files. That can include JavaScript, declarations describing exported types, and source maps connecting generated code to the original source.

With `--noEmit`, you ask it to check without writing those JavaScript or declaration outputs. This fits projects where Vite or a framework already handles the application build.

There is a small distinction around incremental builds: `--noEmit` can still write a `.tsbuildinfo` cache when incremental checking is enabled. The [emit documentation](https://github.com/oxc-project/tsrs/blob/main/docs/EMIT.md) describes how these options interact.

## Why Rust could help

A language choice gives compiler authors different ways to organize memory and parallel work. It doesn't establish a speedup on its own.

The tsrs repository documents optimizations that go beyond translating Go syntax into Rust syntax. They include memory layout changes, checker caches, and changes to how checking work is divided between threads.

### Keeping compiler data compact

A large project creates a lot of compiler objects. There are syntax nodes for the source, symbols for declarations, and types created while resolving expressions and generics.

Those objects refer to each other. The size of those references adds up across a large graph.

The project's [pointer-compression notes](https://github.com/oxc-project/tsrs/blob/main/notes/mem-pointer-compression.md) describe storing compact handles into an arena on supported platforms. An **arena** is a region of memory used to allocate a group of related objects.

Think of a compact handle as an object's position inside that region. Storing smaller handles can reduce the space occupied by structures that contain many references.

There is work involved in turning a handle back into an address. Whether this is worthwhile depends on the memory saved and the access cost. The notes record measurements and tradeoffs, rather than treating smaller references as an automatic win.

### Releasing data when it is no longer needed

Consider a test file that imports application code but isn't imported by any other file. After checking it, the compiler may be able to release its syntax data if nothing else needs it.

The [leaf-file memory notes](https://github.com/oxc-project/tsrs/blob/main/notes/mem-free-leaf-files.md) describe this optimization for eligible files and checking modes. It has conditions because declaration merging, imports, and shared compiler state can keep data reachable.

That explains why reducing peak memory takes more care than freeing each file as soon as its own check finishes. The compiler needs proof that later work won't need the data.

### Building an optimized compiler binary

Release builds also use [profile-guided optimization](https://github.com/oxc-project/tsrs/blob/main/notes/perf-pgo.md). The build process runs representative workloads, records which paths execute, and uses that profile when producing the compiler binary.

This helps explain why a development build from source can behave differently from a published benchmark binary. The build configuration is part of the experiment.

These techniques have their own costs and platform conditions. You can follow the [Rust engineering guide](https://github.com/oxc-project/tsrs/blob/main/docs/RUST.md) for the current implementation choices. I would use project benchmarks to judge the result rather than infer performance from the word “Rust”.

## Reading the benchmark charts

A compiler benchmark depends on the project, compiler revision, and number of checker threads. Changing those can change the result substantially.

Cloudflare says it worked with the TypeScript team to align the announcement's configurations. It also notes that the newer TypeScript beta incorporates improvements, including some contributed upstream from tsrs.

The repository has a separate [historical benchmark report](https://github.com/oxc-project/tsrs/blob/main/bench/RESULTS.md). The chart below uses three workloads from that report. These compare TypeScript 7.0 with a tsrs build following later TypeScript development, so they are separate measurements from the announcement's headline.

The first scenes hold the checker count at four for both compilers. The later scenes use each compiler's defaults on that machine: four checkers for the Go compiler and sixteen for tsrs. You can compare time and peak memory under each configuration.

Both scales start at zero. Lower is better, and the chart keeps the same scale for each metric when changing the configuration.

The report used a Linux machine with 16 vCPUs and 63 GB RAM. Time is the median of ten interleaved runs for tsrs and three for the Go compiler. These are native CLI measurements. They don't measure the browser build.

### Equal threads and default settings answer different questions

An equal-checker comparison helps show what happens when the compilers have the same number of type-checking workers.

A default-settings comparison shows what users may get without tuning anything. If one compiler uses more cores, that is part of its default behavior. It also means the result includes that difference in resource use.

The distinction is visible in memory too. Giving tsrs more checkers can improve elapsed time while increasing its peak memory compared with its four-checker run.

The [benchmark methodology](https://github.com/oxc-project/tsrs/blob/main/bench/README.md) explains the workload preparation and measurements. Here is what I would look for before quoting a result:

| Setting | Why it matters |
| --- | --- |
| Checker threads | Defaults can give the compilers different thread counts |
| TypeScript revision | A language change can produce different errors on identical code |
| Incremental cache | A warm check does different work from a fresh check |
| Files and configuration | Excluding files can make a compiler appear faster |
| Generated dependencies | Missing declarations can stop a check before the real workload |
| Diagnostics | A faster run must still check the intended code correctly |
| Build configuration | Optimized release binaries and development builds can differ |

### Compare the errors too

For the three workloads shown here, the historical report attributes diagnostic differences to upstream changes and checks them against the matching TypeScript development build. It also records a separate tsrs bug in a sixteen-checker Drizzle ORM run: equal error counts, but different error locations.

Matching only the package names isn't enough for a conformance comparison. A Rust port of tomorrow's TypeScript can correctly disagree with today's stable TypeScript.

Also, equal error counts are only an initial check. Two compilers can each report five errors while disagreeing about their locations or meanings. Compare the actual diagnostics when investigating a difference.

### A speedup ratio needs a baseline

Suppose, in a hypothetical project, checking falls from 20 seconds to 5 seconds. That is a fourfold speedup and 15 seconds saved per check.

If a different project falls from 0.20 seconds to 0.05 seconds, the ratio is identical. The effect on your working day is much smaller.

For your own project, record the elapsed time of the command you run repeatedly. If you are investigating CI memory failures, peak process memory may be the more useful measurement.

## Run the native compiler

We'll create a tiny project with one type error. That gives us something concrete to check before trying tsrs on a larger codebase.

Create a folder and install the compiler locally:

```bash
mkdir tsrs-demo
cd tsrs-demo
npm init -y
npm install --save-dev tsrs
```

The npm package selects a native binary for supported systems. You don't need to install Rust to use a published binary. Check the [platform support table](https://github.com/oxc-project/tsrs/blob/main/docs/COMPATIBILITY.md) before setting it up on a different machine.

Save this configuration as `tsconfig.json`:

```json
{
  "compilerOptions": {
    "strict": true,
    "target": "ESNext",
    "module": "ESNext"
  },
  "include": ["app.ts"]
}
```

Then save this code in `app.ts`:

```ts
function greet(name: string) {
  return `Hello, ${name}`
}

console.log(greet(42))
```

Run the check:

```bash
npx tsrs -p . --noEmit
```

The compiler reports that the number argument cannot be assigned to the string parameter. It exits unsuccessfully, which lets a CI job fail when the check finds an error.

Change the last line to `console.log(greet('Flavio'))` and run it again. The error disappears.

`-p .` selects the project in the current directory. `--noEmit` keeps this exercise focused on checking. My guide to [tsconfig.json](https://flaviocopes.com/tsconfig-explained/) explains the configuration options.

### Produce JavaScript and declaration files

To see the compiler's other outputs, use this complete `app.ts`:

```ts
export function greet(name: string) {
  return `Hello, ${name}`
}

console.log(greet('Flavio'))
```

Now compile with declarations enabled and put the output in a separate directory:

```bash
npx tsrs -p . --outDir dist --declaration --noEmitOnError
```

You get `dist/app.js`, with the type annotation removed, and `dist/app.d.ts`, describing the exported function:

```ts
export declare function greet(name: string): string
```

The declaration describes the function's public type. It doesn't contain the function body.

`--noEmitOnError` tells the compiler to withhold outputs if it finds errors. Without that setting, reporting an error and emitting JavaScript are separate decisions.

### Watch and incremental checking

For repeated work, the native CLI supports watch mode:

```bash
npx tsrs -p . --noEmit --watch
```

Keep the terminal open, change the argument, and save the file. The watcher starts another check. Stop it with Ctrl-C.

For separate invocations that reuse build information, you can enable incremental checking:

```bash
npx tsrs -p . --noEmit --incremental
```

The compiler writes build information for later runs. Keep the cache conditions consistent when benchmarking; comparing an incremental run with a fresh full check answers a different question.

The [usage guide](https://github.com/oxc-project/tsrs/blob/main/docs/USAGE.md) covers native watch support and platform conditions. WebAssembly has different limits, which we'll reach shortly.

## Add it to an existing project carefully

I would start by adding a separate command. Keep the existing check working while you compare the new one.

For example, these entries can sit inside an existing `package.json` scripts object:

```json
{
  "typecheck": "tsc -p . --noEmit",
  "typecheck:tsrs": "tsrs -p . --noEmit"
}
```

Run both after the same dependency installation and code-generation steps. If your normal check generates database types first, the tsrs command needs those files too.

A successful check on an empty or incomplete input set tells you very little. Look at the files included by the configuration, particularly in a monorepo with several packages.

Project-reference builds use `-b`, and the compiler supports JavaScript and declaration output across referenced projects. That is another mode to test if your build depends on it. Don't assume a successful `--noEmit` run has validated the declaration files you publish to npm.

### Framework files need their own coverage check

An Astro component, a Vue single-file component, or a Svelte component contains more than ordinary TypeScript source.

A content mapper can present the relevant parts of those files to the compiler. The current compatibility table documents CLI support for content mappers while language-server support remains incomplete.

Keep the framework's checking command until you have verified the same coverage. An ordinary `.ts` check can pass while a mistake in a component template goes unnoticed.

### The editor is a separate integration

Installing tsrs in a project doesn't automatically replace the checker your editor runs.

The native package includes a language server, started with `tsrs --lsp -stdio`. Editors need to launch that server through the right integration.

The [editor setup guide](https://github.com/oxc-project/tsrs/blob/main/docs/LSP.md#editor-setup) documents VS Code's TypeScript native extension and Neovim configuration. Follow that guide for your editor rather than pointing a legacy TypeScript server setting at a Rust executable.

Editor support has its own compatibility tests and gaps. I would compare diagnostics and navigation on the project before changing the team setup. Automatic type acquisition and content-mapper support are among the things to check in the current table.

## Measure your own project

For a quick comparison, install the official compiler alongside tsrs if it isn't already present:

```bash
npm install --save-dev typescript
```

Start with a clean, repeatable type-check command. On macOS or Linux, you can use the shell's `time` command:

```bash
time npx tsc -p . --noEmit --incremental false --pretty false --checkers 4
time npx tsrs -p . --noEmit --incremental false --pretty false --checkers 4
```

These commands assume the official compiler is the native Go-based TypeScript release, which supports `--checkers`. An older TypeScript compiler written in JavaScript won't accept that flag.

Run each several times and alternate their order. Close unusually heavy background jobs. The npm launcher is included in these timings, so very small projects can mostly measure startup overhead.

Record the compiler versions with `npx tsc --version` and `npx tsrs --version`. A comparison with the official stable release can help with an adoption decision. To investigate a correctness difference, compare against the exact upstream revision tsrs targets.

For a macOS peak-memory check, `/usr/bin/time -l` reports maximum resident set size. On Linux with GNU time, `/usr/bin/time -v` reports it too. The output labels and units differ, so keep them with your results.

### Tune only after measuring

The native CLI exposes performance options in addition to the usual TypeScript flags:

| Option | What it controls | Tradeoff |
| --- | --- | --- |
| `--checkers 4` | Number of type-checking workers | More workers can use more memory |
| `--singleThreaded` | Single-threaded execution | Lower concurrency can take longer |
| `--maxMemory 2G` | Memory target for `--noEmit` checking | Recycles checker state and can repeat work |
| `--checkerCostCache .tsrs-costs` | Remember file-checking costs | Adds a cache file for later scheduling |

A memory target is a request to keep the process near that level. It isn't a replacement for an operating-system memory limit or a promise that no allocation can go past it.

If an output difference seems related to checking order, the project's [defaults document](https://github.com/oxc-project/tsrs/blob/main/docs/DEFAULTS.md) explains `--checkerAssignment go` and `--noLazyMembers`. They help compare behavior with the pinned Go implementation, especially around known history and instantiation-depth cases.

I would leave those settings alone during a normal first trial. They are useful when narrowing down a specific discrepancy.

## Why WebAssembly changes where the compiler can run

A native compiler binary is built for a particular operating system and processor. A WebAssembly module runs inside a host that implements the interfaces it needs.

The package [`@tsrs/wasm`](https://github.com/oxc-project/tsrs/blob/main/npm/tsrs-wasm/README.md) builds the shared Rust compiler code for WebAssembly. It exposes a JavaScript API for Node and browsers.

The browser API accepts an in-memory filesystem. You supply filenames and their contents, including the project configuration and dependency declarations. Standard TypeScript library declarations are bundled.

That makes a small online editor possible without running a compiler service on your server. A reader changes an example, presses a button, and gets compiler errors from their own browser.

The scenes show the files arriving, checking inside a worker, the reported error, and the result after correcting the source. A request uses the supplied file contents; editing the textarea alone doesn't change files in an earlier request.

If you haven't used Wasm before, my [WebAssembly tutorial](https://flaviocopes.com/webassembly/) explains modules, imports, and memory.

### Put the compiler in a Web Worker

The browser API is asynchronous while loading the Wasm module. The compilation itself then runs synchronously in that execution context.

Calling an `async` function from the page doesn't automatically move the expensive work off the UI thread. Use a Web Worker so the page can continue handling input while checking runs.

We need to create the browser worker ourselves, which is part of the example below.

### Know which filesystem the compiler sees

A browser worker doesn't receive your laptop's project directory. It sees the virtual files you put into the request.

For one example file, that is easy. For an app importing a library, you need the relevant declarations and resolution paths too. A missing package error may mean your virtual filesystem is incomplete.

`@types/node` also provides declarations rather than Node's runtime. Adding those declarations to a browser compiler would let it understand Node APIs; it wouldn't make those APIs execute in a browser.

### Compiler state and memory have limits

The package caches the compiled Wasm module and creates a fresh instance for each API call. It doesn't keep a live TypeScript program across every keystroke.

Incremental builds can reuse files such as `.tsbuildinfo`. In the in-memory mode, you have to pass the written files back into the next request to reuse them. A worker staying alive doesn't do that automatically.

The current Wasm build runs on one thread. It doesn't support `--watch`, the language server, or the native API-server mode.

Each instance has at most 4 GiB of linear memory, and the host may allow less. That is a Wasm address-space limit, not a statement that every browser can comfortably allocate 4 GiB. Deep recursive types can exhaust the execution stack too.

The API's `memoryBytes` measures peak linear memory, including the module's shadow stack. It isn't the total memory used by the browser tab. Comparing it directly with native process RSS would mix two measurements.

Cloudflare also points to Workers as a reason to pursue Rust and Wasm. A working toolchain there still needs appropriate filesystem and runtime integration. The browser demo below establishes browser support; it doesn't test a Workers deployment.

## Build a browser-based type checker

We'll build a page with a textarea, a Check button, and a result area. The compiler will run in a dedicated worker. We won't execute the submitted program; we only ask the compiler to check it.

Use a recent Node.js release. Create a new directory and install Vite and the Wasm package:

```bash
mkdir tsrs-browser-demo
cd tsrs-browser-demo
npm init -y
npm install --save-dev vite
npm install @tsrs/wasm
```

We use [Vite](https://flaviocopes.com/vite-tutorial/) here to bundle the worker and publish the Wasm asset at a known URL. There are three files to create.

### The page

Save this complete file as `index.html`:

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>TypeScript checker</title>
  </head>
  <body>
    <h1>Check TypeScript in your browser</h1>
    <label for="source">TypeScript source</label>
    <textarea id="source" rows="8" cols="50">const greeting: string = 42</textarea>
    <button id="check" type="button">Check</button>
    <pre id="result" role="status">Ready</pre>
    <script type="module" src="/main.js"></script>
  </body>
</html>
```

The initial program assigns a number to a string. We expect the checker to report that mistake.

### The page's JavaScript

Save this as `main.js`:

```js
const source = document.querySelector('#source')
const button = document.querySelector('#check')
const output = document.querySelector('#result')

const worker = new Worker(new URL('./checker.js', import.meta.url), {
  type: 'module'
})

button.addEventListener('click', () => {
  button.disabled = true
  output.textContent = 'Checking...'
  worker.postMessage(source.value)
})

worker.addEventListener('message', ({ data }) => {
  button.disabled = false
  output.textContent = data
})

worker.addEventListener('error', () => {
  button.disabled = false
  output.textContent = 'The compiler worker failed. Reload the page to retry.'
})
```

The page sends the current textarea contents to the worker. Disabling the button prevents overlapping requests in this small demo.

For an editor that checks automatically while typing, you would also need to handle outdated results. This example checks only when you press the button.

### The compiler worker

Save this as `checker.js`:

```js
import { tsc } from '@tsrs/wasm'
import wasmUrl from '@tsrs/wasm/tsrs.wasm?url'

self.addEventListener('message', async ({ data: source }) => {
  try {
    const result = await tsc(['-p', '/project'], {
      url: wasmUrl,
      cwd: '/project',
      diagnostics: 'json',
      files: {
        '/project/tsconfig.json': JSON.stringify({
          compilerOptions: {
            strict: true,
            noEmit: true,
            target: 'ESNext',
            module: 'ESNext',
            moduleDetection: 'force'
          },
          files: ['app.ts']
        }),
        '/project/app.ts': source
      }
    })

    if (result.exitCode === 0) {
      self.postMessage('No type errors')
      return
    }

    const errors = (result.diagnostics ?? []).map((diagnostic) => {
      const line = diagnostic.startPosition
        ? diagnostic.startPosition.line + 1
        : '?'
      return `Line ${line}: TS${diagnostic.code}: ${diagnostic.text}`
    })

    self.postMessage(
      errors.join('\n') || result.stderr || result.stdout || 'Checking failed'
    )
  } catch (error) {
    self.postMessage(`Could not check the code: ${error.message}`)
  }
})
```

The `?url` import gives us Vite's URL for the Wasm asset. Passing it explicitly avoids depending on the asset remaining beside the bundled JavaScript file.

The virtual `tsconfig.json` enables strict checking and prevents output. `moduleDetection: 'force'` treats the file as a module, keeping its top-level declarations out of the global scope.

That last setting prevents distracting conflicts with browser globals when someone tries names such as `name` in the textarea. It also makes this an explicitly module-based example; remove it when you want to study global-script behavior.

The worker turns compiler diagnostics into readable text and sends them to the page. A compilation failure or Wasm-loading failure also produces a message instead of leaving the page stuck on “Checking...”.

### Run it and correct the error

Start Vite:

```bash
npx vite
```

Open the local URL it prints and press **Check**. You should get TS2322, reporting that a number cannot be assigned to a string.

Change the textarea to:

```ts
const greeting: string = 'Hello'
```

Press Check again. The result should be `No type errors`.

Try an empty textarea too. It is a valid empty module, so it should also pass. That is different from a failed compiler load, which should display an error message.

To check the production bundle, run:

```bash
npx vite build
npx vite preview
```

Open the preview URL and repeat the checks. A development-server success alone doesn't prove that the published worker and `.wasm` URLs are correct.

This demo sends source code between the page and its worker, with no type-checking server. The browser still fetches the application's JavaScript and Wasm asset. If you extend it with remote package loading, analytics, or a backend, examine those network requests before promising that all user code stays local.

## Compatibility before adoption

The project publishes a detailed [compatibility table](https://github.com/oxc-project/tsrs/blob/main/docs/COMPATIBILITY.md). Read the current table when you try it because support can change during the experimental period.

| Area | Current boundary | What I would verify |
| --- | --- | --- |
| Ordinary type checking | Supported, with known conformance exceptions | Same files and diagnostics as the matching upstream compiler |
| JavaScript and declarations | Supported | Published output files and declaration consumers |
| Project references | Supported | Your complete build graph |
| `--init`, `--showConfig` | Unsupported | Scripts that call these commands |
| Language server | Available natively, with documented gaps | Navigation, diagnostics, and the editor integration |
| Framework content mappers | CLI support exists; language-server support is incomplete | Components and templates, alongside `.ts` files |
| WebAssembly | Single-threaded, with separate limits | Worker behavior, asset URLs, and realistic project size |

The current documentation also distinguishes supported platform packages from source builds. Check the actual package available for a target system before changing CI runners or asking every teammate to install it.

A port passing a large upstream test suite is useful evidence. It doesn't mean every project, editor interaction, and runtime configuration has already been exercised.

## How I would use tsrs

For an application with a slow type-checking step, I would add tsrs as a separate CI job first. I would let it run beside the established check across real changes, then investigate any disagreements.

I'd record which TypeScript revision each compiler follows. I would keep dependency installation, generated files, and project selection the same. If there is an error difference, I would reduce it to a small reproduction before deciding which result is correct.

If those checks agree, I would measure the time and memory saved. A smaller CI machine becoming usable could matter more than a dramatic ratio on a workload that was already fast.

For a library, I would compare generated declarations and test a consumer project. For a framework app, I would keep the framework's own check until I could demonstrate equivalent coverage.

For teaching, I would start with the browser demo. A reader could change a union type or a generic function and see the compiler's response beside the lesson. I would keep the example projects small enough to work on ordinary phones and laptops.

What I would avoid initially is replacing the compiler, editor integration, build output, and CI configuration in one change. Keeping those changes separate makes a discrepancy easier to locate and a rollback straightforward.

If your existing check finishes quickly and works reliably, waiting for the stable release is reasonable. The experimental package gives you a way to test the parts that matter to your project now, while keeping the current workflow available.
