This is a collection of common questions about esbuild. You can also ask questions on the GitHub issue tracker.
- Why is esbuild fast?
- Benchmark details
- Upcoming roadmap
- Production readiness
- Anti-virus software
- Outdated version of Go
- Minified newlines
#Why is esbuild fast?
It's written in Go and compiles to native code.
Parallelism is used heavily.
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.
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.
- A pass for lexing, parsing, scope setup, and declaring symbols
- A pass for binding symbols, minifying syntax, JSX/TS to JS, and ESNext-to-ES2015
- 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.
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.
Here are the details about each benchmark:
make bench-three in the esbuild repo.
|rollup 4 + terser
Each time reported is the best of three runs. I'm running esbuild with
--bundle . I used the
@rollup/ plugin because Rollup itself doesn't support minification. Webpack 5 uses
--mode=. 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.
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.
Each time reported is the best of three runs. I'm running esbuild with
--bundle . Webpack 5 uses
--mode=. Parcel 2 uses
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/ but you can't disable type checking, and I tried
@rollup/ but there's no way to provide a
tsconfig.json file (which is required for correct path resolution).
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)
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
- 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.
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
- 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. 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 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.
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:
- Ignore your anti-virus software and remove esbuild from quarantine
- Report the specific esbuild native executable as a false-positive to your anti-virus software vendor
esbuildto bypass your anti-virus software (which likely won't flag WebAssembly files the same way it flags native executables)
- Use another build tool instead of 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.
People are sometimes surprised that esbuild's minifier typically changes 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:
While this code is 18 bytes long:
People are sometimes surprised that esbuild sometimes rewrites top-level
class declarations as
var declarations instead. This is done for a few reasons:
- For correctness
Bundling sometimes needs to lazily-initialize a module. For example, this happens when you call
import()using the path of a module within the bundle. Doing this involves separating the declaration and initialization of top-level symbols by moving the initialization into a closure. So for example
classstatements are rewritten as an assignment of a class expression to a variable. Keeping the declarations out of the lazy-initialization closure is important for performance, since it means other modules can reference them directly instead by name instead of indirectly via a slower property access.
Another case where this is needed is when transforming top-level
usingdeclarations. This involves wrapping the entire module body in a
tryblock, which also involves separating the declaration and initialization of top-level symbols. Top-level symbols may need to be exported, which means they cannot be declared within the
In both of these cases esbuild will fail with a build error if the source code contains a mutation of a
constsymbol, so it's not possible for esbuild's rewriting of top-level
varto result in the mutation of a constant.
Due to esbuild's current architecture, the part of esbuild that does this transformation (the parser) cannot know whether the current module will end up being lazily initialized or not. The information for this decision may only be discovered later on in the build, or may even change in future incremental builds that reuse the same AST (per-file ASTs are transformed once during parsing and then cached and reused across incremental builds). So this transformation is always done when bundling is active.
- For performance
- V8: https://bugs.chromium.org/p/v8/issues/detail?id=13723 (10% slowdown)
classdeclarations all introduce TDZ checks while
vardeclarations do not. Since bundling typically merges many modules into a single very large top-level scope, the performance impact of these TDZ checks can be pretty severe. Converting top-level
varhelps automatically make your code faster.