When we talk about compiling TypeScript projects there are usually two different contexts that are often mixed together: compiling a library vs compiling an application.
Those two contexts have slightly different constraints and need to be considered separately.
Today I'm going to focus on compiling a TypeScript library into a JavaScript package :)
The wishlist
In order to build a nice library that is suited for most projects, we want our package to be:
- type-checked when consumed in a TypeScript project;
- consumable in both
commonjs
andesmodule
projects.
According to those wishes, we need to:
- generate and package the declaration files (
.d.ts
); - be able to compile for different module targets.
Considered solutions
Today, we are going to benchmark the following solutions:
- tsc - the native TypeScript compiler
- esbuild - a full-fledged bundler and superfast compiler written in
go
- swc - a full-fledged bundler and superfast compiler written in
rust
- tsup - a TypeScript library builder based on
esbuild
Side note: I have excluded tools that are only bundlers (webpack
, rollup
, ...) and babel
which was superseded by swc
and esbuild
.
compiler | d.ts generation | multiple module targets |
---|---|---|
esbuild | ❌ | ❌ |
swc | ❌ | ❌ |
tsc | ✅ | ❌ |
tsup | ✅ | ✅ |
By comparing features, there is already something that stands out: only tsup
can generate declarations files (.d.ts
) and target multiple package type out of the box.
This is not a showstopper, though!
Nothing is preventing us from running the compilation twice for different module targets.
And for the d.ts
generation, we could rely on tsc --emitDeclarationOnly
to generate them for us. (Both are actually what tsup
is doing under the hood).
It's worth noting that those processes do not step on each other's toes, so we could run both compilation in parallel :)
Here would be the equivalent commands for all the tools we are going to evaluate:
esbuild
esbuild src/*.ts --format=esm --outdir=./lib/esm &
esbuild src/*.ts --format=cjs --outdir=./lib/cjs &
tsc --emitDeclarationOnly --outDir ./lib/types
tsup
tsup src/*.ts --format esm,cjs --dts -d=./lib
swc
swc -C module.type=es6 src/*.ts -d ./lib/esm &
swc -C module.type=commonjs src/*.ts -d ./lib/cjs &
tsc --emitDeclarationOnly --outDir ./lib/types
tsc
tsc --module esnext --outDir ./lib/esm &
tsc --module commonjs --outDir ./lib/cjs
Benchmark
This benchmark will be all but scientific!
I just tested compiling a library that has a single TypeScript file on GitHub CI. Every compiler is run 30 times, which mean we have very little samples.
This is just to give us a rough idea of how they could perform.
Please take this benchmark with a grain of salt!
(Here are the benchmark sources and the results on Github Actions)
compiler | min | mean | max |
---|---|---|---|
esbuild | 1704ms | 1812ms | 2365ms |
swc | 1794ms | 1855ms | 1908ms |
tsup | 2001ms | 2034ms | 2085ms |
tsc | 2160ms | 2215ms | 2420ms |
Just to highlight how generating d.ts
with tsc
is costly, here is the same benchmark in which we skip generating declaration files:
compiler (no d.ts ) | min | mean | max |
---|---|---|---|
esbuild | 414ms | 427ms | 500ms |
swc | 537ms | 550ms | 570ms |
tsup | 559ms | 570ms | 587ms |
tsc | 2119ms | 2196ms | 2783ms |
This is an order of magnitude different expect for tsc
that seems to yield the same results whether it generates declaration files or not.
There is no obvious winner between swc
, esbuild
and tsup
, but we can see that tsc
is constantly outperformed.
Conclusion
One sad realization that I had while putting up together this benchmark is that no matter how fast the compiler is, it's always going to be at least as slow as tsc --emitDeclarationOnly
.
My personal benchmark isn't really significant when it comes to performances comparisons... But we can clearly identify that tsc
is the loser!
Now, with its built-in support for declaration files generation and multiple targets in my opinion tsup
is the winner! It is tailored for building libraries!
Having only a single command to write to compile our libraries means it easier for maintenance and if, in the future, tsup
figure out a way to generate d.ts
files faster we will benefit from it by upgrading our local version.