native compiler speed without the tsc bottleneck
The official TypeScript compiler tsc is written in JavaScript and optimized for type safety, not speed. esbuild and swc compile the same TypeScript code in Go and Rust respectively, achieving ten to twenty times shorter build times, while deliberately skipping full type checking during transformation.
Table of Contents
- 1. Why tsc becomes a bottleneck for large projects
- 2. The core principle: transpiling instead of type checking
- 3. esbuild in detail: Go performance for TypeScript transforms
- 4. swc in detail: Rust-based compilation with a plugin system
- 5. isolatedModules: the necessary tsconfig safeguard
- 6. Running type checking separately: tsc --noEmit
- 7. Integration into Vite, Jest and build pipelines
- 8. Limits: what esbuild and swc cannot do
- 9. esbuild, swc and tsc directly compared
- 10. Summary
- 11. FAQ
1. Why tsc becomes a bottleneck for large projects
The official TypeScript compiler tsc is itself implemented in TypeScript or JavaScript, and on every build it has to go through the complete type-checking process: it resolves module dependencies, builds a type tree for the entire project, and checks every assignment, function call and interface implementation against that type system. For small projects with a few hundred files, this is not a problem, but for large codebases with several thousand files, a single tsc build can take several minutes, which adds up to significant delays in a CI pipeline with hundreds of builds per day.
The insight that led to the development of esbuild and swc is simple: the actual transformation from TypeScript syntax to JavaScript, meaning stripping type annotations and translating newer language features, is computationally trivial and does not require a full type check. Only the type check itself is the expensive part. If both steps are separated, the pure transformation can run orders of magnitude faster in a natively compiled language like Go or Rust than in JavaScript, while type checking continues to run separately with tsc as needed.
2. The core principle: transpiling instead of type checking
Both esbuild and swc work on the same core principle that TypeScript itself offers via its transpileModule API: each file is parsed on its own and its type annotations are stripped, without actually validating the type against other files in the project. A call to a method that does not exist on an object is compiled without complaint as long as the syntax itself is valid TypeScript. The error would only become visible at runtime as a TypeError, not already at build time.
This fundamental difference from tsc is not a bug but a deliberate design goal: esbuild and swc aim to turn TypeScript source code into runnable JavaScript as fast as possible, while delegating the task of type safety to a separate process. In practice, this means a production setup almost always uses two tools in parallel: a fast transpiler for the actual build, and tsc exclusively for type checking, usually as its own CI step or as an IDE integration.
3. esbuild in detail: Go performance for TypeScript transforms
esbuild, written in Go by Evan Wallace, is primarily a bundler with a built-in TypeScript transpiler that combines parsing, transforming and bundling into a single, highly parallelized process. The performance gains come from several sources at once: Go compiles to native machine code without the interpreter overhead of Node.js, esbuild uses all available CPU cores in parallel for independent files, and the internal AST is kept as a memory-efficient, compact data structure instead of a generic JavaScript object graph.
For TypeScript projects, the esbuild API is usable both via the CLI and programmatically through Node.js, making it the preferred choice for custom build scripts. Its configuration stays deliberately minimal compared to Webpack, but it also forgoes more complex features like a plugin ecosystem of comparable scale. For pure transpilation and bundling tasks in TypeScript projects, esbuild is therefore often the most pragmatic choice when no deep custom transformations of the code are needed.
// build.mjs — esbuild programmatic build script for a TypeScript service
import * as esbuild from 'esbuild';
const result = await esbuild.build({
entryPoints: ['src/main.ts'],
bundle: true,
platform: 'node',
target: 'node20',
format: 'esm',
outfile: 'dist/main.js',
sourcemap: 'linked',
minify: process.env.NODE_ENV === 'production',
external: ['pg-native'], // native modules stay external
metafile: true,
});
// Optional: inspect bundle composition for size regressions
console.log(await esbuild.analyzeMetafile(result.metafile));
4. swc in detail: Rust-based compilation with a plugin system
swc (Speedy Web Compiler), written in Rust, follows an approach similar to esbuild, but differs in two important ways: it offers a more extensive, WASM-based plugin system for custom AST transformations, and it is used by larger frameworks like Next.js as a replacement for Babel, which makes it more deeply integrable into existing transform pipelines. While esbuild primarily acts as a standalone bundler, swc positions itself more as an interchangeable compiler engine within other tools.
For TypeScript projects that need custom code transformations in addition to pure transpilation, such as automatically stripping certain decorators or inserting instrumentation code for observability, swc offers considerably more flexibility than esbuild through its plugin system. This flexibility comes with a somewhat more complex configuration via the .swcrc file, which carries its own JSON structure and conventions compared to esbuild's JavaScript configuration object.
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": false,
"decorators": true
},
"target": "es2022",
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"keepClassNames": true
},
"module": {
"type": "commonjs"
},
"minify": false,
"sourceMaps": true
}
5. isolatedModules: the necessary tsconfig safeguard
Because esbuild and swc transpile every file in isolation, without knowing the context of other files in the project, certain TypeScript features that rely on cross-file type information must be avoided. The most important example is const enum, which the full tsc compiler replaces with literal values across all files at compile time, while an isolated transpiler cannot reliably perform this substitution without the overall context.
The "isolatedModules": true flag in tsconfig.json activates exactly the checks that ensure the TypeScript code can also be transpiled correctly without cross-file context. It warns, for instance, about re-exporting pure types without the export type keyword, since an isolated transpiler cannot tell whether a re-exported identifier is a type or a value, and would in doubt generate a runtime import that fails at runtime. Every project using esbuild or swc instead of tsc for the actual build should enable isolatedModules without exception.
// isolatedModules requires explicit "export type" for pure type re-exports
// WRONG: ambiguous for an isolated transpiler — is UserRole a type or a value?
export { UserRole } from './types';
// RIGHT: explicit type-only re-export, safe for esbuild and swc
export type { UserRole } from './types';
// const enum is NOT allowed with isolatedModules — needs cross-file inlining
// WRONG:
// const enum Status { Active, Inactive }
// RIGHT: regular enum works fine with isolated transpilers
enum Status {
Active = 'ACTIVE',
Inactive = 'INACTIVE',
}
6. Running type checking separately: tsc --noEmit
Since esbuild and swc deliberately do not perform type checking, tsc with the --noEmit option remains the standard way to still guarantee full type safety in the project, without tsc itself producing the actual build output. This command runs the complete type-checking process, reports all type errors, but does not generate .js files, since the actual transpilation is already handled by the faster tool.
In practice, tsc --noEmit usually runs as a separate CI step in parallel with the actual build, or as a local pre-commit hook for the developer, while the fast transpiler is responsible for the daily development loop and the actual deployment. This split means a type error does not immediately block the local development server, which many teams consider an acceptable trade-off, as long as the CI build reliably prevents merging into the main branch when type errors are present.
#!/usr/bin/env bash
# ci.sh — fast build with esbuild, type safety verified in parallel
set -euo pipefail
# Run both steps concurrently, fail the pipeline if either one fails
node build.mjs &
BUILD_PID=$!
npx tsc --noEmit &
TYPECHECK_PID=$!
wait "$BUILD_PID" && wait "$TYPECHECK_PID"
echo "[OK] Build and type check both passed"
7. Integration into Vite, Jest and build pipelines
Vite already uses esbuild by default to transpile TypeScript files during development, which is one of the main reasons for Vite's noticeably shorter cold-start times compared to Webpack-based setups. For production builds, Vite switches to Rollup by default for the actual bundling, but continues to use esbuild for the pure TypeScript-to-JavaScript transformation of individual modules before Rollup merges them.
For test frameworks like Jest, @swc/jest replaces the slower default ts-jest transformer and considerably speeds up test suites with many small test files, since each file is transpiled without the overhead of a full type check. Vitest also uses esbuild internally for transformation, which makes it one of the fastest options for TypeScript test suites out of the box, without requiring additional configuration.
{
"scripts": {
"build": "node build.mjs",
"typecheck": "tsc --noEmit",
"test": "jest",
"ci": "npm run typecheck && npm run build && npm run test"
},
"devDependencies": {
"esbuild": "^0.23.0",
"@swc/core": "^1.7.0",
"@swc/jest": "^0.2.36",
"jest": "^29.7.0",
"typescript": "^5.6.2"
},
"jest": {
"transform": {
"^.+\\.tsx?$": ["@swc/jest"]
}
}
}
8. Limits: what esbuild and swc cannot do
Besides the missing type check, both esbuild and swc have additional limitations that become relevant for certain TypeScript projects. Neither tool can generate declaration files (.d.ts), since generating correct type definitions requires a full type check. Projects published as npm libraries with type definitions therefore still need a separate tsc run with "declaration": true, even if the actual JavaScript output comes from esbuild or swc.
Another difference concerns experimental decorators and metadata reflection, as used by frameworks like NestJS or TypeORM. swc explicitly supports decoratorMetadata through its configuration and is usually the more reliable choice for such frameworks, while esbuild transpiles experimental decorators but without the full metadata emission that some dependency-injection frameworks need at runtime. Before switching from tsc to one of the two native compilers, it is therefore worth testing the entire decorator chain in the specific project.
9. esbuild, swc and tsc directly compared
The choice between these three tools depends on the concrete requirements of the project. The following table compares the most important properties for TypeScript build setups.
| Property | tsc | esbuild | swc |
|---|---|---|---|
| Build speed | Baseline (1x) | 10 to 20x faster | 10 to 20x faster |
| Full type checking | Yes | No | No |
| .d.ts generation | Yes | No | No |
| Plugin system | No (transformer API) | Simple, JS-based | Extensive, WASM-based |
| Decorator metadata | Full | Limited | Fully supported |
| Typical use | Type checking, library builds | Bundling, Vite, fast scripts | Next.js, Jest transform, NestJS |
In practice, these three tools do not exclude each other but complement one another in a well-configured TypeScript setup: esbuild or swc handle the actual, frequently repeated transformation during development and deployment, while tsc --noEmit, running as a separate, less frequent step, guarantees type safety. For libraries with published type definitions, a dedicated tsc run with the declaration option enabled remains additionally necessary.
Mironsoft
Build performance, CI optimization and TypeScript tooling
Ready to noticeably speed up multi-minute TypeScript builds?
We analyze your existing tsc-based build pipeline, introduce esbuild or swc where it makes sense, and set up a clean separation between fast builds and dedicated type checking.
Build audit
Measuring current build times and identifying the biggest bottlenecks
Compiler migration
Switching to esbuild or swc including isolatedModules safeguards
CI integration
Separate, parallel tsc --noEmit step for reliable type safety
10. Summary
esbuild and swc solve a real performance problem for production TypeScript projects by separating the transformation from TypeScript to JavaScript from the expensive type check and running it orders of magnitude faster in a natively compiled language than the JavaScript-based tsc. This speed comes at a price: both tools transpile files in isolation, without cross-file type checking, and therefore require "isolatedModules": true in tsconfig.json as well as a separate tsc --noEmit step for full type safety.
esbuild is particularly well suited for bundling and fast custom build scripts, while swc, with its more extensive plugin system and full decorator metadata support, is the better choice for frameworks like NestJS or Next.js. Neither tool fully replaces tsc, they complement it: the fast compiler handles the daily build, while tsc remains responsible for type checking and declaration-file generation.
TypeScript with esbuild and swc — Key Takeaways
Transpiling over type checking
esbuild and swc strip type annotations in isolation per file, without cross-file type checking.
isolatedModules mandatory
Prevents const enum and ambiguous type re-exports that isolated transpilers cannot safely resolve.
tsc --noEmit separately
Runs as its own CI step for full type safety, without blocking the fast build.
Tool choice
esbuild for bundling and Vite, swc for plugin needs and decorator-heavy frameworks like NestJS.