· 5 min read ·

Project Corsa and the Performance Problem Other Fast TypeScript Tools Never Had to Solve

Source: typescript

Looking back at the December 2025 progress update on TypeScript 7, it’s worth spending some time on what makes Project Corsa different from every other “native TypeScript” tool that came before it, because the distinction is almost always glossed over in the coverage.

The short version: esbuild, SWC, OXC, and Biome are all fast because they skip the hard part. TypeScript 7 doesn’t.

The Easy Version of Native TypeScript

Over the past five years, the JavaScript tooling ecosystem has seen a steady migration toward native-compiled tools. esbuild, written in Go by Evan Wallace, demonstrated in 2020 that a bundler and transpiler for TypeScript could be 10 to 100 times faster than webpack or Babel. SWC, written in Rust, followed a similar path and is now the default transform layer for Next.js. OXC (the Oxidation Compiler) pushed Rust-based parsing and linting further. All of these tools are genuinely impressive.

But none of them type-check. They strip TypeScript’s type annotations and emit JavaScript. They parse the syntax, apply transforms, and discard type information. This is not a criticism; it’s a deliberate scope decision that makes those tools much simpler to build and maintain. TypeScript’s type system, with its conditional types, template literal types, infer in recursive positions, complex generic variance, and structural subtyping, is one of the most sophisticated type inference engines in any production language. esbuild author Evan Wallace has been explicit about this: esbuild’s speed comes partly from not doing the hard thing.

TypeScript 7, codenamed Project Corsa, is the first project to port full, correct TypeScript type-checking to native code. That is what makes it a different kind of engineering problem.

Why the Self-Hosted Compiler Hit a Wall

TypeScript has been self-hosted since its early days: the compiler is written in TypeScript, compiled to JavaScript, and run on Node.js. This was a reasonable choice in 2012. It made the compiler accessible to contributors, it bootstrapped trust in the language, and the Node.js ecosystem handled file I/O and tooling integration without friction.

The problem is that the workload characteristics of type-checking are a poor match for V8’s JIT compiler. V8 is excellent at hot numeric loops, string manipulation, and code paths where objects have predictable, uniform shapes. TypeScript’s type checker is predominantly tree and graph traversal, pointer chasing, and deeply recursive algorithms with highly polymorphic call sites. TypeScript types are represented as plain JavaScript objects with many different shapes; every function in the type checker gets called with a wide variety of those shapes, pushing V8 into megamorphic dispatch mode, which degrades JIT quality significantly.

Beyond JIT behavior, the memory situation is structural. A TypeScript AST node in the JavaScript compiler is a JavaScript object carrying a hidden class pointer, prototype chain, property descriptors, and V8 bookkeeping metadata, even when most of those properties are undefined for a given node type. In Go, the same node is a packed struct sized exactly to what it needs. For large codebases, the Node.js heap for a tsc invocation routinely grows into the 1 to 3 gigabyte range before a major GC is forced. The Go port reduces peak resident memory by roughly 50 to 70 percent.

The single-threaded constraint is the most fundamental limitation. Node.js’s event loop and V8’s inability to share mutable objects across threads meant that no matter how clever the incremental compilation strategy, the compiler could never check independent files or projects in parallel. Modern developer machines have 8, 16, or 32 cores. The JavaScript TypeScript compiler uses one of them.

Why Go, Not Rust

The TypeScript team’s choice of Go over Rust is technically motivated and worth understanding. The type checker is a graph algorithm. TypeScript types form a directed graph with cycles: a type can reference itself through generic instantiation, recursive conditional types, and circular interface definitions. Rust’s ownership and borrow checker makes cyclic data structures genuinely difficult to express, typically requiring Rc<RefCell<T>> wrappers or arena allocation patterns that add significant complexity to code that already has to be semantically identical to the existing compiler.

Go’s garbage collector handles cyclic references naturally, which maps well to the type checker’s need to traverse and cache type graphs with back-edges. Go’s goroutines provide lightweight concurrency that allows independent compilation units to be type-checked in parallel without the data model rewrite that true parallelism would have required in the JavaScript version. And there is a somewhat pleasing symmetry in the choice: Go uses structural interface typing, the same paradigm TypeScript’s type system is built on. The translation from TypeScript to Go is more conceptually natural than it would be to a nominally-typed language like Java or C#.

The approach is not a rewrite from scratch. The team described it as a systematic translation of the existing TypeScript compiler into Go, preserving the same algorithms and data structures. This matters because it means behavioral correctness is verifiable by comparison: the Go compiler and the JavaScript compiler should produce identical type errors on identical inputs. Building confidence in that equivalence is much easier when the code structure is recognizable.

What the Numbers Look Like

The initial announcement in March 2025 cited approximately 10x faster build times on large real-world codebases. The VS Code repository, one of the largest public TypeScript projects, saw type-checking times drop from roughly 77 seconds with tsc on Node.js to roughly 7.5 seconds with the native port. That figure was measured on modern multi-core hardware, where parallelism contributes a significant portion of the gain alongside raw native execution speed.

For the language server (tsserver), the improvements compound. Editor operations like go-to-definition and find-all-references become faster not just because the individual operations are cheaper, but because the language server initializes faster and maintains less memory pressure over a long editing session. V8’s garbage collector pauses, which can cause perceptible latency spikes in editor responses on large codebases, are eliminated entirely.

The Ecosystem Transition

TypeScript 7 ships as a native binary with no Node.js dependency for running tsc or the language server. TypeScript 6.x, the last JavaScript-hosted release line, continues to receive updates during the transition period. The versioning signals the magnitude of the change: this is not a minor feature release but an infrastructure shift.

The December 2025 progress update represents a status check at roughly nine months after the initial announcement, covering compiler and language server porting progress, updated benchmarks, and the path toward an initial public release. The full TypeScript type system is large; porting it correctly takes time, and the team has been explicit that TypeScript 7.0 is a multi-phase effort.

The tools in the ecosystem that do type-stripping (esbuild, SWC, ts-node’s transpile-only mode, Node.js’s native TypeScript support via --experimental-strip-types) are unaffected by any of this. They will remain fast and will continue to be the right choice for build pipelines that separate type-checking from transpilation. What Project Corsa changes is the cost of the type-checking step itself, which has been the dominant factor in TypeScript’s developer experience at scale for years.

The self-hosted compiler was correct for its era. The era where a million-line TypeScript monorepo was unusual ended several years ago. Project Corsa is the belated but necessary response to where the ecosystem actually is.

Was this interesting?