esbuild

Godoc

这是一个 JavaScript 打包和压缩程序。它用于打包 JavaScript 和 TypeScript 代码以在网络上分发。

文档

为什么?

为什么要构建另一个 JavaScript 构建工具?当前用于 Web 的构建工具至少比它们应该实现的效率慢一个数量级。我希望这个项目可以作为 “存在证明”,证明我们的 JavaScript 工具可以更快得多。

基准测试

我想到的用例是打包用于生产的大型代码库。这包括压缩代码以减少网络传输时间,以及生成 source map,这对于调试生产中的错误非常重要。理想情况下,构建工具还应该快速构建,而不必先预热缓存。

我目前有两个基准测试用于衡量 esbuild 的性能。对于这些基准测试,esbuild 比我测试的其他 JavaScript 打包程序 快至少 100 倍

以下是每个基准测试的详细信息:

  • JavaScript 基准测试

    该基准测试通过将 three.js 库复制 10 次并从头开始构建单个打包文件 (bundle) 且不使用任何缓存,从而使代码规模接近一个大型 JavaScript 代码库。可以使用 make bench-three 运行基准测试。

    打包工具 时间 相对 esbuild 的速度 打包速度 输出文件大小
    esbuild 0.36 秒 1 倍 1520.7 kloc/s 5.82 mb
    esbuild(1 个线程) 1.25 秒 4 倍 438.0 kloc/s 5.82 mb
    rollup + terser 36.06 秒 100 倍 15.2 kloc/s 5.80mb
    webpack 44.74 秒 124 倍 12.2 kloc/s 5.97mb
    fuse-box@next 59.52 秒 165 倍 9.2 kloc/s 6.34 mb
    parcel 121.18 秒 337 倍 4.5 kloc/s 5.89 mb

    表中数据是该程序三次运行中最快的一次。我使用了 --bundle --minify --sourcemap 运行 esbuild(单线程版本使用了选项: GOMAXPROCS=1 )。我使用了 rollup-plugin-terser 插件运行 Rollup,因为 Rollup 本身不支持压缩。 Webpack 使用 --mode --mode=production --devtool=sourcemap 。Parcel 使用默认选项。 FuseBox 使用 useSingleBundle: true 配置。绝对速度基于总行数(包括注释和空白行),当前为 547,441。测试是在配备 16GB RAM 的 6 核 2019 MacBook Pro 上完成的。

    注意事项:

    • Parcel :打包程序在运行时因 TypeError: Cannot redefine property: dynamic 错误而崩溃
    • FuseBox:source maps 中的行号似乎偏离了一个
  • TypeScript 基准测试

    此基准使用 Rome 构建工具来模拟大型 TypeScript 代码库。必须将所有代码与 source maps 合并到一个压缩的打包文件中,并且生成的打包文件必须正常工作。可以使用 make bench-rome 运行基准测试。

    打包工具 时间 相对 esbuild 的速度 打包速度 输出文件大小
    esbuild 0.10 秒 1 倍 1287.5 kloc/s 0.98 mb
    esbuild(1 个线程) 0.32 秒 3 倍 412.0 kloc/s 0.98 mb
    parcel 16.77 秒 168 倍 7.9 kloc/s 1.55 mb
    webpack 18.67 秒 187 倍 7.1 kloc/s 1.26 mb

    表中数据是该程序三次运行中最快的一次。我使用了 --bundle --minify --sourcemap --platform=node 运行 esbuild(单线程版本使用了选项: GOMAXPROCS=1 )。 Webpack 将 ts-loadertranspileOnly: true 一起使用 还有 --mode=production --devtool=sourcemap 。Parcel 使用 --target node --bundle-node-modules 。绝对速度基于总行数(包括注释和空白行),当前为 131,836。测试是在配备 16GB RAM 的 6 核 2019 MacBook Pro 上完成的。

    结果中不包括 Rollup,因为我无法使其正常工作。我尝试了 rollup-plugin-typescript@rollup/plugin-typescript@rollup/plugin-sucrase ,但由于与 TypeScript 编译相关的不同原因,它们都不起作用。而且我对 FuseBox 不熟悉,因此我不知道如何解决由于内置 node 模块导致的构建失败。

为什么这么快?

几个原因:

  • 它是用 Go 语言编写的,该语言可以编译为本地代码
  • 解析,生成最终文件和生成 source maps 全部完全并行化
  • 无需昂贵的数据转换,只需很少的几步即可完成所有操作
  • 该库以提高编译速度为编写代码时的第一原则,并尽量避免不必要的内存分配

状态

类似工具:

使用者:

目前支持:

以下是 esbuild 提供的一些主要功能(非详尽列表):

  • 加载器:
  • 压缩
    • 删除空格
    • 缩短标识符
    • 压缩语法
  • 打包
    • ES6 模块的 scope hoisting
    • 遵循 package.json browser 字段
  • Tree shaking
    • 遵循 package.json sideEffects
    • 遵循 /* @__PURE__ */ 注释
  • 输出格式
    • 普通 JS
    • IIFE
    • ES6 模块(支持代码拆分)
  • Source map 生成
  • 将 JSX 和较新的 JS 语法移植到 ES6
  • --define 定义可供编译时替换的名称
  • 打包 JSON 格式的元数据以进行分析

JavaScript 语法支持:

语法转换将较新的 JavaScript 语法转换为较旧的 JavaScript 语法,以与较旧的浏览器一起使用。你可以使用 --target 标志设置语言目标,该标志可以追溯到 ES6。你还可以将 --target 设置为浏览器和其版本的逗号分隔列表(例如 --target=chrome58,firefox57,safari11,edge16 )。请注意,如果使用的语法功能 esbuild 尚不支持转换为当前语言目标,则 esbuild 会在转换不受支持的语法时抛出错误。

以下语法始终针对较旧的浏览器进行转换:

语法转换 语言版本 示例
函数参数列表和调用中的尾部逗号 es2017 foo(a, b, )
数值分隔符 esnext 1_000_000

以下语法会根据配置的目标语言有条件地针对较旧的浏览器进行转换:

语法转换 --target 低于以下语言时转换 示例
求幂运算符 es2016 a ** b
异步函数 es2017 async () => {}
属性拓展运算符 es2018 let x = {...y}
剩余属性 es2018 let {...x} = y
可选的 catch es2019 try {} catch {}
可选链式写法 es2020 a?.b
空位合并 es2020 a ?? b
import.meta es2020 import.meta
类实例字段 esnext class { x }
静态类字段 esnext class { static x }
私有实例方法 esnext class { #x() {} }
私有实例字段 esnext class { #x }
私有静态方法 esnext class { static #x() {} }
私有静态字段 esnext class { static #x }
逻辑赋值运算符 esnext a ??= b
语法转换警告(单击以展开)
  • 空置的合并正确性

    默认情况下 a ?? b 转换为 a != null ? a : b ,之所以有效,是因为 a != null 仅在 anullundefined 为 false。但是,只有一种模糊的边缘情况无法解决。由于遗留原因, document.all 的值是特殊情况,因此 document.all != null 为 false。如果你需要将此值与空值合并运算符一起使用,则应启用 --strict:nullish-coalescing 转换,以便 a ?? b 变成 a !== null && a !== void 0 ? a : b 相反,它可以与 document.all 正常工作。严格来说,默认情况下不会进行严格的转换,因为它会导致在上述边缘情况下代码膨胀,这在现代代码中不重要。

  • 类字段正确性

    类字段如下所示:

    class Foo {
      foo = 123
    }
    

    默认情况下,类字段的转换使用常规赋值方式进行初始化。看起来像这样:

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

    这不会增加太多的代码大小,而且能被现代 JavaScript JIT 高度优化。它还与 TypeScript 编译器的默认行为匹配。但是,这并不完全符合 JavaScript 规范中的初始化行为。例如,如果存在具有该属性名称的设置器,则可能会导致该设置器被调用,这是不应该发生的。更加准确的转换将是使用 Object.defineProperty () 代替,如下所示:

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

    但是这会增加代码体积并降低性能,但会更准确地遵循 JavaScript 规范。如果需要遵循这种规范,可以启用 --strict:class-fields 选项或将 useDefineForClassFields 标志添加到 tsconfig.json 文件。

  • 私有属性性能问题

    此转换使用 WeakMapWeakSet 保留此语法的隐私属性,类似于 Babel 和 TypeScript 编译器中的相应转换。对于大型 WeakMapWeakSet 对象,大多数现代 JavaScript 引擎(V8,JavaScriptCore 和 SpiderMonkey,不包括 ChakraCore)可能没有很好的优化。在此语法转换处于活动状态的情况下,使用私有字段或私有方法创建许多类的实例可能会导致垃圾回收器大量开销。这是因为现代引擎(ChakraCore 除外)将弱值存储在实际的映射对象中,而不是将其存储为键本身的隐藏属性,并且大型映射对象可能会导致垃圾回收的性能问题。有关更多信息,请参见此参考

请注意,如果要对所有转换启用严格性,则可以直接设置--strict 而不是对每个转换都使用 --strict:...

以下语法当前不支持转换,会原样输出:

语法转换 --target 低于以下语言时转换 示例
异步迭代 es2018 for await (let x of y) {}
异步 generator es2018 async function* foo() {}
BigInt es2020 123n
Hashbang 语法 esnext #!/usr/bin/env node

尚不支持以下语法,目前无法对其进行解析:

语法转换 语言版本 示例
顶层 await esnext await import(x)

另请参阅 完成的 ECMAScript 提议 活跃的 ECMAScript 提议列表

TypeScript 语法支持:

通过删除类型注释并将仅 TypeScript 支持的语法特性转换为 JavaScript 代码来转换 TypeScript 文件。本节记录了对仅 TypeScript 拥有的语法特性的支持。请参考上 一节 以获取对 JavaScript 语法特性的支持,该语法特性也适用于 TypeScript 文件。

请注意,esbuild 不会进行任何类型检查。你仍然需要使用 tsc -noEmit 类的工具来运行类型检查。

以下是仅 TypeScript 支持的语法特性受支持的情况,它们会始终被转换为 JavaScript(非完整列表):

语法特性 示例 注释
Namespace namespace Foo {}
Enums enum Foo { A, B }
Const enums const enum Foo { A, B } 与常规 enums 相同
通用类型参数 <T>(a: T): T => a
带有类型的 JSX <Element<T>/>
类型转换 a as B<B>a
imports 类型 import {Type} from 'foo' 通过删除所有未使用的 imports 来处理
exports 类型 export {Type} from 'foo' 通过忽略 TypeScript 文件中丢失的 exports 进行处理
实验性质的装饰器 @sealed class Foo {} 不支持使用 emitDecoratorMetadata 标志选项

以下这些仅 TypeScript 所有的语法特性将被解析和忽略(非完整列表):

语法特性 示例
接口声明 interface Foo {}
类型声明 type Foo = number
函数声明 function foo(): void;
环境声明 declare module 'foo' {}
仅类型的 imports import type {Type} from 'foo'
仅类型的 exports export type {Type} from 'foo'

免责声明:

  • 据我所知,尚未有人将其用于生产。它仍然是相当新的代码,你可能会遇到一些错误。

    也就是说,已经在处理 JavaScript 和 TypeScript 语言规范中的各种边缘情况方面付出了很多努力,并且 esbuild 在许多现实世界的代码库中都能很好地工作。例如,esbuild 可以打包 rollup sucrase ,和 esprima (所有这些都是使用 TypeScript 编写)以及所生成的打包文件通过了所有测试。如果你发现语言支持方面的问题(尤其是实际代码),请报告此问题以便得到解决。

  • 这个项目仍处于初期阶段,我想至少在目前为止保持范围相对集中。我正在尝试创建一个构建工具,该构建工具 a)在部分给定的用例上运行良好,并且 b)刷新社区对于 JavaScript 构建工具一定非常慢的固有印象。我并不是要创建一个可以构建任何东西的极其灵活的构建系统。

    也就是说,esbuild 现在具有 JavaScript APIGo API 。可以用作压缩 JavaScript,将 TypeScript / JSX 转换为 JavaScript 或将较新的 JavaScript 转换为较旧的 JavaScript 的库。因此,即使 esbuild 不支持特定技术,esbuild 仍然可以集成为库来帮助加快速度。例如, ViteSnowpack 最近开始使用 esbuild 的转换库来添加对 TypeScript 的支持(官方 TypeScript 编译器太慢了)。

  • 我目前主要是在寻求使用反馈,而不是贡献代码。该项目尚处于初期阶段,我仍在努力开发一个 MVP 打包器,该打包器可以合理地替换实际的工具链。尚有一些主要的基础要素(例如 CSS 支持,watch 模式,代码拆分)尚未到位,并且它们都需要协同工作才能达到一流的性能。

安装

可以使用 npm 安装预构建的二进制文件:

  • 本地安装(推荐)

    这会将 esbuild 命令本地安装在项目的 package.json 文件中:

    npm install --save-dev esbuild
    

    使用 npx esbuild [arguments] 调用它。请注意,这使用的是 npx 包运行命令,而不是 npm 包管理器命令。

    这是推荐的基于项目的工作流程,因为它允许你为每个项目使用不同版本的 esbuild ,并确保从事相同项目的每个人都具有相同版本的 esbuild

  • 全局安装

    这会在 PATH 中添加一个名为 esbuild 的全局命令:

    npm install --global esbuild
    

    使用 esbuild [arguments] 调用它。

    如果要在项目上下文之外运行 esbuild 来执行一次性文件操作任务,则进行全局安装会很方便。

esbuild 软件包应在 64 位 macOS,Linux 和 Windows 系统上运行。它包含一个安装脚本,该脚本下载适用于当前平台的适当软件包。如果安装脚本无法正常运行,或者你需要在不受支持的平台上运行 esbuild,则有一个名为 esbuild-wasm 的后备 WebAssembly 软件包应可在所有平台上运行。

其他安装方法(单击展开)

仅为了方便起见,二进制文件托管在 npm 上。由于 esbuild 是原生二进制文件,因此无需安装 npm 即可使用 esbuild。

使用 HTTP 下载

可以直接通过 HTTP 从 npm 仓库下载二进制文件,而无需先安装 npm。

  • 对于 Linux(二进制文件将是 ./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
    
  • 对于 macOS(二进制文件为 ./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
    
  • 对于 Windows(二进制文件为 ./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
    

0.0.0 替换为要下载的 esbuild 版本。

使用 Go 安装

如果你已安装 Go 编译器工具链,则可以使用它来全局安装 esbuild

GO111MODULE=on go get github.com/evanw/esbuild/cmd/esbuild@v0.0.0

二进制文件将放置在 Go 的全局二进制目录中(该目录名为 bin 位于 go env GOPATH 命令返回的目录内)。你可能需要将该 bin 目录添加到 PATH 。用 v0.0.0 替换要构建的 esbuild 版本。

从源代码构建

你也可以从源代码构建 esbuild:

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

这将在当前目录创建一个名为esbuild ( 在 Windows 上面是esbuild.exe)的二进制文件。用 v0.0.0 替换要构建的 esbuild 版本。

命令行用法

命令行接口会获取入口文件列表,并为每个入口文件生成一个打包文件。以下是可用的选项:

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, chrome80)
  --platform=...        Platform target (browser or node, default browser)
  --external:M          Exclude module M from the bundle
  --format=...          Output format (iife, cjs, esm)
  --splitting           Enable code splitting (currently only for esm)
  --color=...           Force use of color terminal escapes (true or false)
  --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)
  --resolve-extensions=...  A comma-separated list of implicit extensions
  --metafile=...            Write metadata about the build to a JSON file
  --strict                  Transforms handle edge cases but have more overhead
  --pure=N                  Mark the name N as a pure function for tree shaking

  --trace=...           Write a CPU trace to this file
  --cpuprofile=...      Write a CPU profile to this file

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

与 React 一起使用

要将 esbuild 与 React 一起使用:

  • 可以将所有 JSX 语法放在 .jsx 文件而不是 .js 文件中,或者使用 --loader:.js=jsx.js 文件使用 JSX 加载器。

  • 如果你使用的是 TypeScript,请直接传递给 esbuild .tsx 文件作为入口文件。无需先将 TypeScript 文件转换为 JavaScript,因为 esbuild 本身会解析 TypeScript 语法。

    请注意,esbuild 不执行任何类型检查,因此你需要并行运行 tsc -noEmit 来检查类型。

  • 如果你使用 esbuild 打包 React,而不是将其包含在 HTML 的<script> 标签中,则需要在命令行上传递 '--define:process.env.NODE_ENV="development"''--define:process.env.NODE_ENV="production"' 以进行构建。

    请注意, "production" 周围的双引号很重要,因为替换应为字符串,而不是标识符。外部单引号用于转义 Bash 中的双引号,但在其他 shell 中可能没有必要。

  • 如果你使用的是 Preact 而不是 React,则需要配置 JSX 工厂。你可以在命令行上传递 --jsx-factory=preact.h --jsx-fragment=preact.Fragment 进行构建,也可以添加 "jsxFactory": "preact.h", "jsxFragmentFactory": "preact.Fragment"tsconfig.json 文件。

例如,如果你有一个名为 example.tsx 的文件,其内容如下:

import * as React from 'react'
import * as ReactDOM from 'react-dom'

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
);

使用它进行开发环境的构建:

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

将此用于生产环境的构建:

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