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:

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
parcel 2
rollup 4 + terser
webpack 5

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.39s 1x 1403.7 kloc/s 5.80mb
parcel 2 14.91s 38x 36.7 kloc/s 5.78mb
rollup 4 + terser 34.10s 87x 16.1 kloc/s 5.82mb
webpack 5 41.21s 106x 13.3 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
parcel 2
webpack 5

This benchmark uses the old Rome code base (prior to their Rust rewrite) 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 6.91ѕ 69x 16.1 kloc/s 0.96mb
webpack 5 16.69ѕ 167x 8.3 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 for reasons relating to TypeScript compilation. I tried @rollup/plugin-typescript but you can't disable type checking, and I tried @rollup/plugin-sucrase but there's no way to provide a tsconfig.json file (which is required for correct path resolution).

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:

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:

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:

Anti-virus software

Since esbuild is written in native code, anti-virus software can sometimes incorrectly flag it as a virus. This does not mean esbuild is a virus. I do not publish malicious code and I take supply chain security very seriously.

Virtually all of esbuild's code is first-party code except for one dependency on Google's set of supplemental Go packages. My development work is done on different machine that is isolated from the one I use to publish builds. I have done additional work to ensure that esbuild's published builds are completely reproducible and after every release, published builds are automatically compared to ones locally-built in an unrelated environment to ensure that they are bitwise identical (i.e. that the Go compiler itself has not been compromised). You can also build esbuild from source yourself and compare your build artifacts to the published ones to independently verify this.

Having to deal with false-positives is an unfortunate reality of using anti-virus software. Here are some possible workarounds if your anti-virus won't let you use esbuild:

Outdated version of Go

If you use an automated dependency vulnerability scanner, you may get a report that the version of the Go compiler that esbuild uses and/or the version of golang.org/x/sys (esbuild's only dependency) is outdated. These reports are benign and should be ignored.

This happens because esbuild's code is deliberately intended to be compilable with Go 1.13. Later versions of Go have dropped support for certain older platforms that I want esbuild to be able to run on (e.g. older versions of macOS). While esbuild's published binaries are compiled with a much newer version of the Go compiler (and therefore don't work on older versions of macOS), you are currently still able to compile the latest version of esbuild for yourself with Go 1.13 and use it on older versions of macOS because esbuild's code can still be compiled with Go as far back as 1.13.

People and/or automated tools sometimes see the go 1.13 line in go.mod and complain that esbuild's published binaries are built with Go 1.13, which is a really old version of Go. However, that's not true. That line in go.mod only specifies the minimum compiler version. It has nothing to do with the version of Go that esbuild's published binaries are built with, which is a much newer version of Go. Please read the documentation.

People also sometimes want esbuild to update the golang.org/x/sys dependency because there is a known vulnerability in the version that esbuild uses (specifically GO-2022-0493 about the Faccessat function). The problem that prevents esbuild from updating to a newer version of the golang.org/x/sys dependency is that newer versions have started using the unsafe.Slice function, which was first introduced in Go 1.17 (and therefore doesn't compile in older versions of Go). However, this vulnerability report is irrelevant because a) esbuild doesn't ever call that function in the first place and b) esbuild is a build tool, not a sandbox, and esbuild's file system access is not security-sensitive.

I'm not going to drop compatibility with older platforms and prevent some people from being able to use esbuild just to work around irrelevant vulnerability reports. Please ignore any reports about the issues described above.

Minified newlines

People are sometimes surprised that esbuild's minifier typically changes the character escape sequence \n within JavaScript strings into a newline character in a template literal. But this is intentional. This is not a bug with esbuild. The job of a minifier is to generate as compact an output as possible that's equivalent to the input. The character escape sequence \n is two bytes long while a newline character is one byte long.

For example, this code is 21 bytes long:

var text="a\nb\nc\n";

While this code is 18 bytes long:

var text=`a

So the second code is fully minified while the first one isn't. Minifying code does not mean putting it all on one line. Instead, minifying code means generating equivalent code that uses as few bytes as possible. In JavaScript, an untagged template literal is equivalent to a string literal, so esbuild is doing the correct thing here.

Avoiding name collisions

Top-level variables in an entry point module should never end up in the global scope when running esbuild's output in a browser. If that happens, it means you did not follow esbuild's documentation about output formats and are using esbuild incorrectly. This is not a bug with esbuild.

Specifically, you must do either one of the following when running esbuild's output in a browser:

  1. --format=iife with <script src="...">

    If you are running your code in the global scope, then you should be using --format=iife. This causes esbuild's output to wrap your code so that top-level variables are declared in a nested scope.

  2. --format=esm with <script src="..." type="module">

    If you are using --format=esm, then you must run your code as a module. This causes the browser to wrap your code so that top-level variables are declared in a nested scope.

Using --format=esm with <script src="..."> will break your code in subtle and confusing ways (omitting type="module" means that all top-level variables will end up in the global scope, which will then collide with top-level variables that have the same name in other JavaScript files).

Top-level var

People are sometimes surprised that esbuild sometimes rewrites top-level let, const, and class declarations as var declarations instead. This is done for a few reasons:

Note that esbuild doesn't preserve top-level TDZ side effects because modules may need to be lazily initialized (as described above), which means separating declaration from initialization. TDZ checks for top-level symbols could hypothetically still be supported by generating extra code that checks before each use of a top-level symbol and throws if it hasn't been initialized yet (effectively manually implementing what a real JavaScript VM would do). However, this seems like an excessive overhead for both code size and run time, and does not seem like something that a production-oriented bundler should do.