Faster Builds with esbuild
Jeff Straney·
Most TypeScript projects run tsc to turn .ts files into .js. That works. It also takes forever.
tsc does two things: it type checks, and it transpiles. The type checking is slow and necessary. The transpiling part is not. tsc is conservative and thorough, which is perfect for type safety and wrong for build speed.
esbuild does the transpiling part orders of magnitude faster. It doesn't type check. That's the deal: you give up type checking in the build step, and in exchange you get a build that finishes in milliseconds instead of seconds.
The way to use this is simple: run tsc --noEmit as a type check step, and run esbuild for the actual build. You keep all the type safety. You just split the jobs so the slow job doesn't block the fast one.
The Numbers
On a modest TypeScript codebase, tsc alone takes 2-3 seconds. esbuild with the same output takes 50-100ms. That is not a 10% speedup. That is a 25-30x speedup.
The catch is that tsc was doing two things and esbuild is doing one. Once you add the type check step back, you're looking at 2-3 seconds total (the type check time) instead of 2-3 seconds for just the build. But in practice, you only run the full pipeline when you cut a release. During development, you run esbuild and get instant feedback while your editor runs the type checker in the background.
How to Wire It
Your package.json build script should look like this:
{
"scripts": {
"type-check": "tsc --noEmit",
"build": "esbuild src/index.ts --outfile=dist/index.js --bundle --platform=node",
"build:ci": "npm run type-check && npm run build"
}
}
For TypeScript files with JSX, add the JSX flag:
{
"scripts": {
"build": "esbuild src --outdir=dist --bundle --platform=node --jsx=transform"
}
}
The --platform=node tells esbuild you're building for Node, not the browser. It will not bundle Node built-ins like fs or path. Without it, esbuild tries to bundle everything and fails.
The --jsx=transform tells esbuild how to handle JSX. If you're using React, you'd use --jsx=automatic and point it to the React runtime. For KitaJS (which this site uses), --jsx=transform calls the JSX factory directly, which is what you want.
The Constraints
esbuild does not type check. If you forget a variable, misspell a method, or pass the wrong type, esbuild will still emit the JavaScript. The type checker running separately (in your editor, or in CI with tsc --noEmit) is what catches those errors.
esbuild also does not do incremental builds. Every invocation rebuilds the whole bundle. tsc in watch mode is incremental, so if you change one file it only recompiles that file. esbuild reruns the whole bundle each time. For small-to-medium projects this is still fast enough that it does not matter. For very large projects, esbuild's plugin system lets you write custom logic to handle incremental builds, but that is not the default.
The third thing: esbuild's output is not identical to tsc's output. It uses different conventions for module interop, it handles some edge cases differently, and the generated code looks different. In practice this almost never matters, but if you have code that depends on the exact shape of the transpiled output, you might find surprises.
Why This Matters
The speed difference between a 2-second build and a 100ms build is not 2 seconds. It is the difference between paying attention and giving up. If you hit save and wait 2 seconds, your brain has already context-switched. If you hit save and the build is done, you stay in flow.
This is not a huge thing. It is not the thing that decides whether a project succeeds. It is the compound effect of a thousand small delays that either add up to friction or do not. esbuild removes friction. That is worth the five minutes it takes to wire it up.
I was skeptical of esbuild when it came out. Seemed like premature optimization, replacing a tool that worked fine for a tool that was three times faster at something that was not a bottleneck. After using it, I was wrong. The bottleneck is not one 3-second build. The bottleneck is the accumulated weight of a hundred times hitting save and waiting that builds into resistance to even opening the project.
esbuild fixed that. It is worth using.
