esbuild

by evanw

evanw / esbuild

An extremely fast JavaScript bundler and minifier

14.7K Stars 306 Forks Last release: Not found MIT License 1.3K Commits 111 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

esbuild

Godoc

This is a JavaScript bundler and minifier. It packages up JavaScript and TypeScript code for distribution on the web.

Documentation

Why?

Why build another JavaScript build tool? The current build tools for the web are at least an order of magnitude slower than they should be. I'm hoping that this project serves as an "existence proof" that our JavaScript tooling can be much, much faster.

Benchmarks

The use case I have in mind is packaging a large codebase for production. This includes minifying the code, which reduces network transfer time, and producing source maps, which are important for debugging errors in production. Ideally the build tool should also build quickly without having to warm up a cache first.

I currently have two benchmarks that I'm using to measure esbuild performance. For these benchmarks, esbuild is at least 10-100x faster than the other JavaScript bundlers I tested:

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
.
| Bundler            |    Time | Relative slowdown | Absolute speed | Output size |
| :----------------- | ------: | ----------------: | -------------: | ----------: |
| esbuild            |   0.37s |                1x |  1479.6 kloc/s |      5.81mb |
| esbuild (1 thread) |   1.54s |                4x |   355.5 kloc/s |      5.81mb |
| rollup + terser    |  36.00s |               97x |    15.2 kloc/s |      5.81mb |
| webpack 4          |  41.91s |              113x |    13.1 kloc/s |      5.97mb |
| [email protected]      |  53.10s |              144x |    10.3 kloc/s |      6.34mb |
| webpack 5          |  63.83s |              173x |     8.6 kloc/s |      5.84mb |
| parcel 2           | 108.66s |              294x |     5.0 kloc/s |      5.81mb |
| parcel 1           | 118.51s |              320x |     4.6 kloc/s |      5.89mb |

Each time reported is the best of three runs. I'm running esbuild with --bundle --minify --sourcemap (the single-threaded version uses GOMAXPROCS=1). I used the rollup-plugin-terser plugin because Rollup itself doesn't support minification. Webpack uses --mode=production --devtool=sourcemap. Parcel uses the default options. FuseBox is configured with useSingleBundle: true. 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.

Caveats:

  • Parcel: The bundle crashes at run time with TypeError: Cannot redefine property: dynamic
  • Parcel 2: Must be given extra memory with node --max-old-space-size or it runs out of memory
  • FuseBox: The line numbers in source maps appear to be off by one
  • #### 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
.
| Bundler            |    Time | Relative slowdown | Absolute speed | Output size |
| :----------------- | ------: | ----------------: | -------------: | ----------: |
| esbuild            |   0.11s |                1x |  1198.5 kloc/s |      0.98mb |
| esbuild (1 thread) |   0.38s |                3x |   346.9 kloc/s |      0.98mb |
| parcel 1           |  15.43s |              140x |     8.5 kloc/s |      1.55mb |
| webpack 4          |  18.03s |              164x |     7.3 kloc/s |      1.26mb |
| webpack 5          |  23.85s |              217x |     5.5 kloc/s |      1.26mb |

Each time reported is the best of three runs. I'm running esbuild with --bundle --minify --sourcemap --platform=node (the single-threaded version uses GOMAXPROCS=1). Webpack uses ts-loader with transpileOnly: true and --mode=production --devtool=sourcemap. Parcel uses --target node --bundle-node-modules. 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.

The results don't include Rollup because I couldn't get it to work. I tried rollup-plugin-typescript, @rollup/plugin-typescript, and @rollup/plugin-sucrase and they all didn't work for different reasons relating to TypeScript compilation. And I'm not familiar with FuseBox so I'm not sure how work around build failures due to the use of builtin node modules. Parcel 2 is excluded because it cannot currently build the benchmark.

Why is it fast?

Several reasons:

  • It's written in Go, a language that compiles to native code
  • Parsing, printing, and source map generation are all fully parallelized
  • Everything is done in very few passes without expensive data transformations
  • Code is written with speed in mind, and tries to avoid unnecessary allocations

Status

Similar tools:

Used by:

Currently supported:

Here are some of the main features that esbuild offers (a non-exhaustive list):

  • Loaders:
  • Minification
    • Remove whitespace
    • Shorten identifiers
    • Compress syntax
  • Bundling
    • Scope hoisting for ES6 modules
    • Respects
      browser
      overrides in
      package.json
  • Tree shaking
    • Respects
      sideEffects
      in
      package.json
    • Respects
      /* @__PURE__ */
      annotations
  • Output formats
    • CommonJS
    • IIFE
    • ES6 module (supports code splitting)
  • Source map generation
  • Transpilation of JSX and newer JS syntax down to ES6
  • Compile-time name substitution with
    --define
  • Bundle metadata in JSON format for analysis

JavaScript syntax support:

Syntax transforms convert newer JavaScript syntax to older JavaScript syntax for use with older browsers. You can set the language target with the

--target
flag, which goes back as far as ES6. You can also set
--target
to a comma-separated list of browsers and versions (e.g.
--target=chrome58,firefox57,safari11,edge16
). Note that if you use a syntax feature that esbuild doesn't yet have support for transforming to your current language target, esbuild will generate an error where the unsupported syntax is used.

These syntax features are always transformed for older browsers:

| Syntax transform | Language version | Example | |--------------------------------------------------------------------------------------------------------------------|------------------|---------------| | Trailing commas in function parameter lists and calls |

es2017
|
foo(a, b, )
| | Numeric separators |
esnext
|
1_000_000
|

These syntax features are conditionally transformed for older browsers depending on the configured language target:

| Syntax transform | Transformed when

--target
is below | Example | |-------------------------------------------------------------------------------------|--------------------------------------|----------------------------| | Exponentiation operator |
es2016
|
a ** b
| | Async functions |
es2017
|
async () => {}
| | Spread properties |
es2018
|
let x = {...y}
| | Rest properties |
es2018
|
let {...x} = y
| | Optional catch binding |
es2019
|
try {} catch {}
| | Optional chaining |
es2020
|
a?.b
| | Nullish coalescing |
es2020
|
a ?? b
| |
import.meta
|
es2020
|
import.meta
| | Class instance fields |
esnext
|
class { x }
| | Static class fields |
esnext
|
class { static x }
| | Private instance methods |
esnext
|
class { #x() {} }
| | Private instance fields |
esnext
|
class { #x }
| | Private static methods |
esnext
|
class { static #x() {} }
| | Private static fields |
esnext
|
class { static #x }
| | Logical assignment operators |
esnext
|
a ??= b
|
Syntax transform caveats (click to expand)
  • Nullish coalescing correctness

    By default a ?? b is transformed into a != null ? a : b, which works because a != null is only false if a is null or undefined. However, there's exactly one obscure edge case where this doesn't work. For legacy reasons, the value of document.all is special-cased such that document.all != null is false. If you need to use this value with the nullish coalescing operator, you should enable --strict:nullish-coalescing transforms so a ?? b becomes a !== null && a !== void 0 ? a : b instead, which works correctly with document.all. The strict transform isn't done by default because it causes code bloat for an obscure edge case that shouldn't matter in modern code.

  • Optional chaining correctness

    By default a?.b is transformed into a == null ? void 0 : a.b, which works because a == null is only true if a is neither null nor undefined. However, there's exactly one obscure edge case where this doesn't work. For legacy reasons, the value of document.all is special-cased such that document.all == null is true. If you need to use this value with optional chaining, you should enable --strict:optional-chaining transforms so a?.b becomes a === null || a === void 0 ? void 0 : a.b instead, which works correctly with document.all. The strict transform isn't done by default because it causes code bloat for an obscure edge case that shouldn't matter in modern code.

  • Class field correctness

    Class fields look like this:

    class Foo {
      foo = 123
    }

    By default, the transform for class fields uses a normal assignment for initialization. That looks like this:

    class Foo {
      constructor() {
        this.foo = 123;
      }
    }

    This doesn't increase code size by much and stays on the heavily-optimized path for modern JavaScript JITs. It also matches the default behavior of the TypeScript compiler. However, this doesn't exactly follow the initialization behavior in the JavaScript specification. For example, it can cause a setter to be called if one exists with that property name, which isn't supposed to happen. A more accurate transformation would be to use Object.defineProperty() instead like this:

    class Foo {
      constructor() {
        Object.defineProperty(this, "foo", {
          enumerable: true,
          configurable: true,
          writable: true,
          value: 123
        });
      }
    }

    This increases code size and decreases performance, but follows the JavaScript specification more accurately. If you need this accuracy, you should either enable the --strict:class-fields option or add the useDefineForClassFields flag to your tsconfig.json file.

  • Private member performance

    This transform uses WeakMap and WeakSet to preserve the privacy properties of this feature, similar to the corresponding transforms in the Babel and TypeScript compilers. Most modern JavaScript engines (V8, JavaScriptCore, and SpiderMonkey but not ChakraCore) may not have good performance characteristics for large WeakMap and WeakSet objects. Creating many instances of classes with private fields or private methods with this syntax transform active may cause a lot of overhead for the garbage collector. This is because modern engines (other than ChakraCore) store weak values in an actual map object instead of as hidden properties on the keys themselves, and large map objects can cause performance issues with garbage collection. See this reference for more information.

Note that if you want to enable strictness for all transforms, you can just pass --strict instead of using --strict:... for each transform.

These syntax features are currently always passed through un-transformed:

| Syntax transform | Unsupported when

--target
is below | Example | |-------------------------------------------------------------------------------------|--------------------------------------|-----------------------------| | Asynchronous iteration |
es2018
|
for await (let x of y) {}
| | Async generators |
es2018
|
async function* foo() {}
| | BigInt |
es2020
|
123n
| | Hashbang grammar |
esnext
|
#!/usr/bin/env node
| | Top-level await |
esnext
|
await import(x)
|

Note that while transforming code containing top-level await is supported, bundling code containing top-level await is not yet supported.

See also the list of finished ECMAScript proposals and the list of active ECMAScript proposals.

Transforming ES6+ syntax to ES5 is not supported yet. However, if you're using esbuild to transform ES5 code, you should still set

--target=es5
. This prevents esbuild from introducing ES6 syntax into your ES5 code. For example, without this flag the object literal
{x: x}
will become
{x}
and the string
"a\nb"
will become a multi-line template literal when minifying. Both of these substitutions are done because the resulting code is shorter, but the substitutions will not be performed if
--target=es5
is present.

TypeScript syntax support:

TypeScript files are transformed by removing type annotations and converting TypeScript-only syntax features to JavaScript code. This section documents the support of TypeScript-only syntax features. Please refer to the previous section for support of JavaScript syntax features, which also applies in TypeScript files.

Note that esbuild does not do any type checking. You will still want to run type checking using something like

tsc -noEmit
.

These TypeScript-only syntax features are supported, and are always converted to JavaScript (a non-exhaustive list):

| Syntax feature | Example | Notes | |-------------------------|----------------------------|-------| | Namespaces |

namespace Foo {}
| | | Enums |
enum Foo { A, B }
| | | Const enums |
const enum Foo { A, B }
| Behaves the same as regular enums | | Generic type parameters |
(a: T): T => a
| | | JSX with types |
/>
| | | Type casts |
a as B
and
a
| | | Type imports |
import {Type} from 'foo'
| Handled by removing all unused imports | | Type exports |
export {Type} from 'foo'
| Handled by ignoring missing exports in TypeScript files | | Experimental decorators |
@sealed class Foo {}
| The
emitDecoratorMetadata
flag is not supported |

These TypeScript-only syntax features are parsed and ignored (a non-exhaustive list):

| Syntax feature | Example | |------------------------|---------------------------------| | Interface declarations |

interface Foo {}
| | Type declarations |
type Foo = number
| | Function declarations |
function foo(): void;
| | Ambient declarations |
declare module 'foo' {}
| | Type-only imports |
import type {Type} from 'foo'
| | Type-only exports |
export type {Type} from 'foo'
|

Disclaimers:

  • As far as I know, this hasn't yet been used in production by anyone yet. It's still pretty new code and you may run into some bugs.

    That said, a lot of effort has gone into handling all of the various edge cases in the JavaScript and TypeScript language specifications and esbuild works well on many real-world code bases. For example, esbuild can bundle rollup, sucrase, and esprima (all TypeScript code bases) and the generated bundles pass all tests. If you find an issue with language support (especially with real-world code), please report it so it can get fixed.

  • This project is still pretty early and I'd like to keep the scope relatively focused, at least for now. I'm trying to create a build tool that a) works well for a given sweet spot of use cases and b) resets the expectations of the community for what it means for a JavaScript build tool to be fast. I'm not trying to create an extremely flexible build system that can build anything.

    That said, esbuild now has a JavaScript API and a Go API. This can be used as a library to minify JavaScript, convert TypeScript/JSX to JavaScript, or convert newer JavaScript to older JavaScript. So even if esbuild doesn't support a particular technology, it's possible that esbuild can still be integrated as a library to help speed it up. For example, Vite and Snowpack recently started using esbuild's transform library to add support for TypeScript (the official TypeScript compiler was too slow).

  • I'm mainly looking for feedback at the moment, not contributions. The project is early and I'm still working toward an MVP bundler that can reasonably replace real-world toolchains. There are still major fundamental pieces that haven't been put in place yet (e.g. CSS support, watch mode, code splitting) and they all need to work well together to have best-in-class performance.

Install

A prebuilt binary can be installed using npm:

  • Local install (recommended)

    This installs the

    esbuild
    command locally in your project's
    package.json
    file:
    npm install --save-dev esbuild
    

    Invoke it using

    npx esbuild [arguments]
    . Note that this uses the
    npx
    package runner command, not the
    npm
    package manager command.

    This is the recommended project-based workflow because it allows you to have a different version of

    esbuild
    for each project and it ensures that everyone working on a given project has the same version of
    esbuild
    .
  • Global install

    This adds a global command called

    esbuild
    to your path:
    npm install --global esbuild
    

    Invoke it using

    esbuild [arguments]
    .

    A global install can be handy if you want to run

    esbuild
    outside of a project context for one-off file manipulation tasks.

The

esbuild
package should work on 64-bit macOS, Linux, and Windows systems. It contains an install script that downloads the appropriate package for the current platform. If the install script isn't working or you need to run esbuild on an unsupported platform, there is a fallback WebAssembly package called esbuild-wasm that should work on all platforms.
Other installation methods (click to expand)

The binary is only hosted on npm for convenience. Since esbuild is a native binary, it's not necessary to install npm to use esbuild.

Download using HTTP

The binary can be downloaded from the npm registry directly over HTTP without needing to install npm first.

  • For Linux (the binary will be ./package/bin/esbuild):

    curl -O https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.0.0.tgz
    tar xf ./esbuild-linux-64-0.0.0.tgz
  • For macOS (the binary will be ./package/bin/esbuild):

    curl -O https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.0.0.tgz
    tar xf ./esbuild-darwin-64-0.0.0.tgz
  • For Windows (the binary will be ./package/esbuild.exe):

    curl -O https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.0.0.tgz
    tar xf ./esbuild-windows-64-0.0.0.tgz

Substitute 0.0.0 for the version of esbuild that you want to download.

Install using Go

If you have the Go compiler toolchain installed, you can use it to install the esbuild command globally:

GO111MODULE=on go get github.com/evanw/esbuild/cmd/[email protected]

The binary will be placed in Go's global binary directory (the directory called bin located inside the directory returned by go env GOPATH). You may need to add that bin directory to your PATH. Substitute v0.0.0 for the version of esbuild that you want to build.

Build from source

You can also build esbuild from source:

git clone --depth 1 --branch v0.0.0 https://github.com/evanw/esbuild.git
cd esbuild
go build ./cmd/esbuild

This will create a binary called esbuild (esbuild.exe on Windows) in the current directory. Substitute v0.0.0 for the version of esbuild that you want to build.

Command-line usage

The command-line interface takes a list of entry points and produces one bundle file per entry point. Here are the available options:

Usage:
  esbuild [options] [entry points]

Options: --bundle Bundle all dependencies into the output files --outfile=... The output file (for one entry point) --outdir=... The output directory (for multiple entry points) --sourcemap Emit a source map --target=... Environment target (e.g. es2017, chrome58, firefox57, safari11, edge16, node10, default esnext) --platform=... Platform target (browser | node, default browser) --external:M Exclude module M from the bundle --format=... Output format (iife | cjs | esm, no default when not bundling, otherwise default is iife when platform is browser and cjs when platform is node) --splitting Enable code splitting (currently only for esm) --global-name=... The name of the global for the IIFE format

--minify Sets all --minify-* flags --minify-whitespace Remove whitespace --minify-identifiers Shorten identifiers --minify-syntax Use equivalent but shorter syntax

--define:K=V Substitute K with V while parsing --jsx-factory=... What to use instead of React.createElement --jsx-fragment=... What to use instead of React.Fragment --loader:X=L Use loader L to load file extension X, where L is one of: js | jsx | ts | tsx | json | text | base64 | file | dataurl | binary

Advanced options: --version Print the current version and exit --sourcemap=inline Emit the source map with an inline data URL --sourcemap=external Do not link to the source map with a comment --sourcefile=... Set the source file for the source map (for stdin) --error-limit=... Maximum error count or 0 to disable (default 10) --log-level=... Disable logging (info | warning | error | silent, default info) --resolve-extensions=... A comma-separated list of implicit extensions (default ".tsx,.ts,.jsx,.mjs,.cjs,.js,.css,.json") --metafile=... Write metadata about the build to a JSON file --strict Transforms handle edge cases but have more overhead (enable individually using --strict:X where X is one of: nullish-coalescing | optional-chaining | class-fields) --pure:N Mark the name N as a pure function for tree shaking --inject:F Import the file F into all input files and automatically replace matching globals with imports --tsconfig=... Use this tsconfig.json file instead of other ones --out-extension:.js=.mjs Use a custom output extension instead of ".js" --main-fields=... Override the main file order in package.json (default "browser,module,main" when platform is browser and "main,module" when platform is node) --public-path=... Set the base URL for the "file" loader --color=... Force use of color terminal escapes (true | false) --avoid-tdz An optimization for large bundles in Safari

Examples:

Produces dist/entry_point.js and dist/entry_point.js.map

esbuild --bundle entry_point.js --outdir=dist --minify --sourcemap

Allow JSX syntax in .js files

esbuild --bundle entry_point.js --outfile=out.js --loader:.js=jsx

Substitute the identifier RELEASE for the literal true

esbuild example.js --outfile=out.js --define:RELEASE=true

Provide input via stdin, get output via stdout

esbuild --minify --loader=ts < input.ts > output.js

Using with React

To use esbuild with React:

  • Either put all JSX syntax in

    .jsx
    files instead of
    .js
    files, or use
    --loader:.js=jsx
    to use the JSX loader for
    .js
    files.
  • If you're using TypeScript, pass esbuild your

    .tsx
    file as the entry point. There should be no need to convert TypeScript files to JavaScript first because esbuild parses TypeScript syntax itself.

    Note that esbuild does not do any type checking, so you'll want to run

    tsc -noEmit
    in parallel to check types.

  • If you're using esbuild to bundle React yourself instead of including it with a

     tag in your HTML, you'll need to pass 
    '--define:process.env.NODE_ENV="development"'
    or
    '--define:process.env.NODE_ENV="production"'
    to esbuild on the command line.

    Note that the double quotes around

    "production"
    are important because the replacement should be a string, not an identifier. The outer single quotes are for escaping the double quotes in Bash but may not be necessary in other shells.

  • If you're using Preact instead of React, you'll need to configure the JSX factory. You can either pass

    --jsx-factory=h --jsx-fragment=Fragment
    to esbuild on the command line, or add
    "jsxFactory": "h", "jsxFragmentFactory": "Fragment"
    to your
    tsconfig.json
    file. You will also have to add
    import {h, Fragment} from 'preact'
    in files containing JSX syntax.

For example, if you have a file called

example.tsx
with the following contents:
import * as React from 'react'
import * as ReactDOM from 'react-dom'

ReactDOM.render(

Hello, world!

, document.getElementById('root') );

Use this for a development build:

esbuild example.tsx --bundle '--define:process.env.NODE_ENV="development"' --outfile=out.js

Use this for a production build:

esbuild example.tsx --bundle '--define:process.env.NODE_ENV="production"' --minify --outfile=out.js

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.