MLog
Back to posts
前端#TypeScript#Go#编译器#性能优化

TypeScript 7.0's Go Rewrite: Paying Off a Decade of Engineering Debt

Published: Jul 15, 2026Reading time: 8 min

TypeScript 7.0 RC ships with the compiler ported from TypeScript to Go, delivering 8-12x faster builds on large projects. A look at the engineering decisions behind the rewrite, where the performance actually comes from, and what it means for the JS ecosystem.

TypeScript 7.0 RC dropped last week. Every headline says "10x faster." That's the headline number. But the interesting stuff is underneath.

I upgraded a mid-size monorepo—around 2,000 .ts files—from 6.0 to 7.0 RC over the weekend. tsc --noEmit went from 47 seconds to just over 5. I'll admit I thought the benchmarks were inflated until I ran my own.

What grabbed me wasn't the number. It was the series of engineering decisions the TypeScript team made to get there: why Go instead of Rust, how they solved deterministic parallel type-checking, why the file watcher had to be rewritten from scratch. That chain of reasoning is more instructive than any benchmark chart.

Not a new version. A new material.

Let's be clear about what TypeScript 7.0 actually is: it introduces zero new type system features. The type-checking logic is structurally identical to 6.0. In TypeScript PM Daniel Rosenwasser's words, this was a "methodical port"—not a rewrite from scratch.

What that means in practice: the TypeScript compiler logic, originally written in TypeScript, was reimplemented line by line in Go. Same architecture, same inference paths, same error message formatting. The runtime just changed from V8 to a native Go binary.

This is fundamentally different from esbuild's approach. Evan Wallace built a bundler from scratch in Go, designing his own data structures and algorithms. TypeScript 7.0 is more like a massive transcompilation effort. The goal wasn't to reinvent type-checking—it was to make the existing type-checking run faster.

This distinction matters. It explains why the team could ship a release candidate roughly six months after announcing the port. It also explains why there are virtually no behavioral changes—because it's the same compiler with a different engine.

Where the performance actually comes from

A common misunderstanding: "10x faster" means Go is 10x faster than JavaScript. That's not what's happening. V8's JIT can run JavaScript very fast. On single-threaded compute-bound tasks, Go won't beat V8 by an order of magnitude.

The real gains come from three places:

First, native parallelism. TypeScript's build pipeline has three stages: parsing, type-checking, and emit. In the JavaScript version, all three run on a single main thread. In Go, parsing and emit are almost entirely independent across files and can be parallelized with zero friction.

The hard part is type-checking. A file's type information depends on everything it imports—you can't just scatter files across threads and hope for the best. Their solution is clever: create a fixed pool of type-checker workers (defaulting to 4), each with its own complete "worldview." They might duplicate some work—like checking global type declarations—but because the input is identical and the partitioning strategy is deterministic, the results are always the same.

This isn't the theoretically optimal parallelization scheme. But it's the engineering-correct one. No complex dependency graph scheduling, no distributed consistency headaches. It just works.

Second, the absence of GC pauses. JavaScript's garbage collector becomes a real problem at scale. A TypeScript compiler chewing through a large codebase can easily consume gigabytes of memory, and GC pauses directly impact end-to-end latency. Go's concurrent GC isn't zero-overhead either, but it's dramatically more controllable in this scenario.

Third, startup time. Every invocation of tsc as a CLI tool requires Node.js to load the entire runtime—parse tsconfig, initialize the module system, warm up the JIT. A Go binary starts almost instantly. For CI pipelines that invoke tsc --noEmit dozens of times, this gap gets multiplied considerably.

The actual numbers, from the official announcement:

Project TypeScript 6 TypeScript 7 Speedup
VS Code 125.7s 10.6s 11.9x
Sentry 139.8s 15.7s 8.9x
Bluesky 24.3s 2.8s 8.7x
Playwright 12.8s 1.47s 8.7x
tldraw 11.2s 1.46s 7.7x

The VS Code editing experience tells the real story: the time from opening a file with errors to seeing the first diagnostic dropped from 17.5 seconds to under 1.3 seconds. That's not "faster"—that's going from "I'll tab over to something else while it loads" to "instant."

An underrated story: the Parcel watcher cross-language port

The file watcher in tsc --watch has been a persistent performance bottleneck. Node.js's fs.watch behaves differently across operating systems and imposes significant overhead on large node_modules directories.

The TypeScript team needed an efficient, cross-platform filesystem watcher. Their choice was interesting: port Parcel's watcher—a mature C++ implementation—to Go.

This decision reveals a few things:

  1. They didn't want to introduce a C++ toolchain dependency. TypeScript ships as an npm package that needs to compile seamlessly to every platform. Go's cross-compilation is the natural fit here.
  2. They recognized the design of Parcel's watcher as battle-tested. It's been running inside VS Code for years.
  3. After the initial port, they progressively "Go-ified" it—from a literal translation to idiomatic Go. This is engineering pragmatism: get something working first, then make it beautiful.

Parcel author Devon Govett received a special acknowledgment. A C++ → Go → TypeScript feedback loop isn't something you see often in open source. It's worth appreciating.

Why Go, not Rust?

This has been the most-asked question since the Go port was announced. The JavaScript ecosystem has been trending Rust: esbuild uses Go (the outlier), but swc, Rome/Biome, Oxc, Rspack, and Rolldown all chose Rust. It looked like Rust was the "correct" answer.

My take: the TypeScript compiler is not a bundler, and it's not a parser library. It's a type system—it has to handle deeply complex control flow analysis, generic inference, conditional type distribution. This kind of logic is highly branchy, state-dense, and hard to express elegantly in Rust's ownership model.

Rust's borrow checker would make the process of literally translating type-checking logic from JavaScript agonizing. You'd need to redesign data structures and ownership models from scratch—which is no longer a port, it's a full rewrite. The TypeScript team was explicit: they didn't want a rewrite. They wanted "same semantics, faster execution."

Go's goroutine + channel + shared memory model, combined with a simpler syntax that sidesteps ownership concerns, made the literal port feasible. It was a pragmatic engineering choice: Rust might yield marginally better peak performance, but at 2-3x the effort and with a real risk of introducing behavioral differences.

The bigger picture: JS infrastructure is leaving JavaScript

TypeScript 7.0 isn't an isolated case. Look at what's happened over the past few years:

Project Original Lang Target Lang Status
esbuild Go Production
swc Rust Production
Rome → Biome TypeScript Rust Production
Oxc Rust Production
Rspack Rust Production
Rolldown Rust In development
Parcel 2 JavaScript Rust Released
TypeScript (tsc) TypeScript Go 7.0 RC

This isn't a coincidence. Build tools, compilers, and linters share a common trait: they're CPU-bound, embarrassingly parallel, cache-friendly workloads—exactly the kind of work JavaScript runtimes are worst at. JavaScript was never optimized for this. It excels at I/O-bound async tasks (this is Node.js's entire reason for existing), not saturating 8 cores with computation.

In other words: JavaScript isn't bad. It's just that using a screwdriver to hammer nails was always a bit awkward.

Migration: go through 6.0 first

If you're still on TypeScript 5.x, don't jump straight to 7.0. TypeScript 7.0 elevates 6.0's deprecation warnings to hard errors. Here's the path:

  1. Upgrade to 6.0 and resolve all deprecation warnings.
  2. Make sure rootDir, types, moduleResolution, and other key settings are correct.
  3. Run a full type-check in CI.
  4. Then install 7.0 RC: npm install -D typescript@rc

If your project depends on typescript-eslint or other compiler API consumers, note that 7.0 doesn't provide a stable programmatic API—that's coming in 7.1. During the transition, use the @typescript/typescript6 compatibility package:

{
  "devDependencies": {
    "typescript": "npm:@typescript/typescript6@^6.0.2",
    "@typescript/native": "npm:typescript@^7.0.2"
  }
}

This isn't a long-term dual-version strategy—it's a bridge to give ecosystem tooling time to adapt.

Closing

The most exciting thing about TypeScript 7.0 isn't the 10x speedup you can see right now. It's the engineering headroom the Go foundation unlocks for the future.

For a decade, every new feature the TypeScript team considered came with the same anxious question: "How much slower will this make tsc?" That constraint is now gone. Future releases can pursue more aggressive type inference improvements, smarter error diagnostics, larger-scale incremental builds—and none of it requires fighting V8's garbage collector anymore.

This isn't a performance release. It's a foundation release for the next ten years.