This is a collection of common questions about esbuild. You can also ask questions on the GitHub issue tracker.

Why is esbuild fast?

Several reasons:

  • It's written in Go and compiles to native code.

    Most other bundlers are written in JavaScript, but a command-line application is a worst-case performance situation for a JIT-compiled language. Every time you run your bundler, the JavaScript VM is seeing your bundler's code for the first time without any optimization hints. While esbuild is busy parsing your JavaScript, node is busy parsing your bundler's JavaScript. By the time node has finished parsing your bundler's code, esbuild might have already exited and your bundler hasn't even started bundling yet.

    In addition, Go is designed from the core for parallelism while JavaScript is not. Go has shared memory between threads while JavaScript has to serialize data between threads. Both Go and JavaScript have parallel garbage collectors, but Go's heap is shared between all threads while JavaScript has a separate heap per JavaScript thread. This seems to cut the amount of parallelism that's possible with JavaScript worker threads in half according to my testing, presumably since half of your CPU cores are busy collecting garbage for the other half.

  • Parallelism is used heavily.

    The algorithms inside esbuild are carefully designed to fully saturate all available CPU cores when possible. There are roughly three phases: parsing, linking, and code generation. Parsing and code generation are most of the work and are fully parallelizable (linking is an inherently serial task for the most part). Since all threads share memory, work can easily be shared when bundling different entry points that import the same JavaScript libraries. Most modern computers have many cores so parallelism is a big win.

  • Everything in esbuild is written from scratch.

    There are a lot of performance benefits with writing everything yourself instead of using 3rd-party libraries. You can have performance in mind from the beginning, you can make sure everything uses consistent data structures to avoid expensive conversions, and you can make wide architectural changes whenever necessary. The drawback is of course that it's a lot of work.

    For example, many bundlers use the official TypeScript compiler as a parser. But it was built to serve the goals of the TypeScript compiler team and they do not have performance as a top priority. Their code makes pretty heavy use of megamorphic object shapes and unnecessary dynamic property accesses (both well-known JavaScript speed bumps). And the TypeScript parser appears to still run the type checker even when type checking is disabled. None of these are an issue with esbuild's custom TypeScript parser.

  • Memory is used efficiently.

    Compilers are ideally mostly O(n) complexity in the length of the input. So if you are processing a lot of data, memory access speed is likely going to heavily affect performance. The fewer passes you have to make over your data (and also the fewer different representations you need to transform your data into), the faster your compiler will go.

    For example, esbuild only touches the whole JavaScript AST three times:

    1. A pass for lexing, parsing, scope setup, and declaring symbols
    2. A pass for binding symbols, minifying syntax, JSX/TS to JS, and ESNext-to-ES2015
    3. A pass for minifying identifiers, minifying whitespace, generating code, and generating source maps

    This maximizes reuse of AST data while it's still hot in the CPU cache. Other bundlers do these steps in separate passes instead of interleaving them. They may also convert between data representations to glue multiple libraries together (e.g. string→TS→JS→string, then string→JS→older JS→string, then string→JS→minified JS→string) which uses more memory and slows things down.

    Another benefit of Go is that it can store things compactly in memory, which enables it to use less memory and fit more in the CPU cache. All object fields have types and fields are packed tightly together so e.g. several boolean flags only take one byte each. Go also has value semantics and can embed one object directly in another so it comes "for free" without another allocation. JavaScript doesn't have these features and also has other drawbacks such as JIT overhead (e.g. hidden class slots) and inefficient representations (e.g. non-integer numbers are heap-allocated with pointers).

Each one of these factors is only a somewhat significant speedup, but together they can result in a bundler that is multiple orders of magnitude faster than other bundlers commonly in use today.

Benchmark details

Here are the details about each benchmark:

JavaScript benchmark

This benchmark approximates a large JavaScript codebase by duplicating the three.js library 10 times and building a single bundle from scratch, without any caches. The benchmark can be run with make bench-three in the esbuild repo.

Bundler Time Relative slowdown Absolute speed Output size
esbuild 0.33s 1x 1658.9 kloc/s 5.80mb
parcel 2 32.48s 98x 16.9 kloc/s 5.87mb
rollup + terser 34.95s 106x 15.7 kloc/s 5.81mb
webpack 5 41.53s 126x 13.2 kloc/s 5.84mb

Each time reported is the best of three runs. I'm running esbuild with --bundle --minify --sourcemap. I used the rollup-plugin-terser plugin because Rollup itself doesn't support minification. Webpack 5 uses --mode=production --devtool=sourcemap. Parcel 2 uses the default options. Absolute speed is based on the total line count including comments and blank lines, which is currently 547,441. The tests were done on a 6-core 2019 MacBook Pro with 16gb of RAM and with macOS Spotlight disabled.

TypeScript benchmark

This benchmark uses the Rome build tool to approximate a large TypeScript codebase. All code must be combined into a single minified bundle with source maps and the resulting bundle must work correctly. The benchmark can be run with make bench-rome in the esbuild repo.

Bundler Time Relative slowdown Absolute speed Output size
esbuild 0.10s 1x 1318.4 kloc/s 0.97mb
parcel 2 8.73s 87x 15.1 kloc/s 0.97mb
webpack 5 18.89s 189x 7.0 kloc/s 1.27mb

Each time reported is the best of three runs. I'm running esbuild with --bundle --minify --sourcemap --platform=node. Webpack 5 uses ts-loader with transpileOnly: true and --mode=production --devtool=sourcemap. Parcel 2 uses "engines": "node" in package.json. Absolute speed is based on the total line count including comments and blank lines, which is currently 131,836. The tests were done on a 6-core 2019 MacBook Pro with 16gb of RAM and with macOS Spotlight disabled.

The results don't include Rollup because I couldn't get it to work. I tried @rollup/plugin-typescript and @rollup/plugin-sucrase but they both failed with build errors relating to TypeScript miscompilation.

Upcoming roadmap

These features are already in progress and are first priority:

These are potential future features but may not happen or may happen to a more limited extent:

  • HTML content type (#31)
  • Lowering to ES5 (#297)
  • Bundling top-level await (#253)

After that point, I will consider esbuild to be relatively complete. I'm planning for esbuild to reach a mostly stable state and then stop accumulating more features. This will involve saying "no" to requests for adding major features to esbuild itself. I don't think esbuild should become an all-in-one solution for all frontend needs. In particular, I want to avoid the pain and problems of the "webpack config" model where the underlying tool is too flexible and usability suffers.

For example, I am not planning to include these features in esbuild's core itself:

  • Support for other frontend languages (e.g. Elm, Svelte, Vue, Angular)
  • TypeScript type checking (just run tsc separately)
  • An API for custom AST manipulation
  • Hot-module reloading
  • Module federation

I hope that the extensibility points I'm adding to esbuild (plugins and the API) will make esbuild useful to include as part of more customized build workflows, but I'm not intending or expecting these extensibility points to cover all use cases. If you have very custom requirements then you should be using other tools. I also hope esbuild inspires other build tools to dramatically improve performance by overhauling their implementations so that everyone can benefit, not just those that use esbuild.

I am planning to continue to maintain everything in esbuild's existing scope even after esbuild reaches stability. This means implementing support for newly-released JavaScript and TypeScript syntax features, for example.

Production readiness

This project has not yet hit version 1.0.0 and is still in active development. That said, it is far beyond the alpha stage and is pretty stable. I think of it as a late-stage beta. For some early-adopters that means it's good enough to use for real things. Some other people think this means esbuild isn't ready yet. This section doesn't try to convince you either way. It just tries to give you enough information so you can decide for yourself whether you want to use esbuild as your bundler.

Some data points:

  • Used by other projects

    The API is already being used as a library within many other developer tools. For example, Vite and Snowpack are using esbuild to transform TypeScript into JavaScript and Amazon CDK (Cloud Development Kit) and Phoenix are using esbuild to bundle code.

  • API stability

    Even though esbuild's version is not yet 1.0.0, effort is still made to keep the API stable. Patch versions are intended for backwards-compatible changes and minor versions are intended for backwards-incompatible changes (as recommended by npm). If you plan to use esbuild for something real, you should either pin the exact version (maximum safety) or pin the major and minor versions (only accept backwards-compatible upgrades).

  • Only one main developer

    This tool is primarily built by me. For some people this is fine, but for others this means esbuild is not a suitable tool for their organization. That's ok with me. I'm building esbuild because I find it fun to build and because it's the tool I'd want to use. I'm sharing it with the world because there are others that want to use it too, because the feedback makes the tool itself better, and because I think it will inspire the ecosystem to make better tools.

  • Not always open to scope expansion

    I'm not planning on including major features that I'm not interested in building and/or maintaining. I also want to limit the project's scope so it doesn't get too complex and unwieldy, both from an architectural perspective, a testing and correctness perspective, and from a usability perspective. Think of esbuild as a "linker" for the web. It knows how to transform and bundle JavaScript and CSS. But the details of how your source code ends up as plain JavaScript or CSS may need to be 3rd-party code.

    I'm hoping that plugins will allow the community to add major features (e.g. WebAssembly import) without needing to contribute to esbuild itself. However, not everything is exposed in the plugin API and it may be the case that it's not possible to add a particular feature to esbuild that you may want to add. This is intentional; esbuild is not meant to be an all-in-one solution for all frontend needs.

© 2020 Evan Wallace
Licensed under the MIT License.