常见问题解答

这是一个关于 esbuild 的常见问题解答集。你也可以在 GitHub 问题跟踪器 上提问。

为什么 esbuild 速度很快?

几个原因

这些因素中的每一个都只是略微提高了速度,但它们加在一起可以产生一个打包器,其速度比目前常用的其他打包器快几个数量级。

基准测试详情

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

JavaScript 基准测试
esbuild
0.39s
parcel 2
14.91s
rollup 4 + terser
34.10s
webpack 5
41.21s
0s
10s
20s
30s
40s

此基准测试通过将 three.js 库复制 10 次并从头开始构建一个包来模拟一个大型 JavaScript 代码库,没有任何缓存。可以在 esbuild 仓库 中使用 make bench-three 运行基准测试。

打包器 时间 相对减速 绝对速度 输出大小
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

每次报告的时间都是三次运行中最好的。我使用 --bundle --minify --sourcemap 运行 esbuild。我使用了 @rollup/plugin-terser 插件,因为 Rollup 本身不支持最小化。Webpack 5 使用 --mode=production --devtool=sourcemap。Parcel 2 使用默认选项。绝对速度基于总行数,包括注释和空行,目前为 547,441 行。测试是在一台 6 核 2019 款 MacBook Pro 上进行的,该机器配备 16gb 内存,并且禁用了 macOS Spotlight

TypeScript 基准测试
esbuild
0.10s
parcel 2
6.91s
webpack 5
16.69s
0s
5s
10s
15s

此基准测试使用旧的 Rome 代码库(在他们使用 Rust 重写之前)来模拟一个大型 TypeScript 代码库。所有代码必须合并到一个最小化的包中,并带有源映射,并且生成的包必须正常工作。可以在 esbuild 仓库 中使用 make bench-rome 运行基准测试。

打包器 时间 相对减速 绝对速度 输出大小
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

每次报告的时间都是三次运行中最好的。我使用 --bundle --minify --sourcemap --platform=node 运行 esbuild。Webpack 5 使用 ts-loader,并使用 transpileOnly: true--mode=production --devtool=sourcemap。Parcel 2 在 package.json 中使用 "engines": "node"。绝对速度基于总行数,包括注释和空行,目前为 131,836 行。测试是在一台 6 核 2019 款 MacBook Pro 上进行的,该机器配备 16gb 内存,并且禁用了 macOS Spotlight

结果不包括 Rollup,因为我无法让它工作,原因与 TypeScript 编译有关。我尝试了 @rollup/plugin-typescript,但你无法禁用类型检查,我还尝试了 @rollup/plugin-sucrase,但没有办法提供 tsconfig.json 文件(这是正确路径解析所必需的)。

未来路线图

这些功能正在开发中,并且是首要任务

这些是潜在的未来功能,但可能不会实现,或者实现范围更小

在那之后,我认为 esbuild 已经相对完整了。我计划让 esbuild 达到一个基本稳定的状态,然后停止添加更多功能。这将涉及对添加 esbuild 本身的主要功能的请求说“不”。我认为 esbuild 不应该成为所有前端需求的一站式解决方案。特别是,我想避免“webpack 配置”模型的痛苦和问题,在这种模型中,底层工具过于灵活,导致可用性下降。

例如,我计划在 esbuild 的核心本身中包含这些功能

我希望我正在添加到 esbuild 中的可扩展性点(插件API)将使 esbuild 有用,可以将其包含在更定制的构建工作流程中,但我并不打算或期望这些可扩展性点涵盖所有用例。如果你有非常定制的要求,那么你应该使用其他工具。我还希望 esbuild 能激励其他构建工具通过彻底改造它们的实现来大幅提高性能,这样每个人都能从中受益,而不仅仅是那些使用 esbuild 的人。

我计划在 esbuild 达到稳定状态后,继续维护 esbuild 现有范围内的所有内容。这意味着实现对新发布的 JavaScript 和 TypeScript 语法功能的支持,例如。

生产就绪

该项目尚未达到 1.0.0 版本,并且仍在积极开发中。也就是说,它已经远远超出了 alpha 阶段,并且非常稳定。我认为它处于一个晚期的 beta 阶段。对于一些早期采用者来说,这意味着它已经足够好,可以用于实际事物。其他人认为这意味着 esbuild 还没有准备好。本节不会试图说服你任何一方。它只是试图为你提供足够的信息,以便你能够自己决定是否要使用 esbuild 作为你的打包器。

一些数据点

防病毒软件

由于 esbuild 是用原生代码编写的,因此防病毒软件有时会错误地将其标记为病毒。这并不意味着 esbuild 是病毒。我不会发布恶意代码,并且非常重视供应链安全。

esbuild 的几乎所有代码都是第一方代码,除了对 Google 的补充 Go 包集的 一个依赖项。我的开发工作是在与我用来发布构建的机器不同的隔离的机器上完成的。我已经做了额外的工作来确保 esbuild 的发布构建是完全可重现的,并且在每次发布后,发布的构建都会 自动与 在无关环境中本地构建的构建进行比较,以确保它们是按位相同的(即 Go 编译器本身没有被破坏)。您也可以自己从源代码构建 esbuild,并将您的构建工件与发布的工件进行比较,以独立验证这一点。

不得不处理误报是使用防病毒软件的一个不幸现实。如果您的防病毒软件不允许您使用 esbuild,以下是一些可能的解决方法

过时的 Go 版本

如果您使用自动依赖项漏洞扫描器,您可能会收到有关 esbuild 使用的 Go 编译器版本和/或 golang.org/x/sys(esbuild 的唯一依赖项)版本过时的报告。这些报告是良性的,应该忽略。

发生这种情况是因为 esbuild 的代码故意旨在与 Go 1.13 兼容。更高版本的 Go 已放弃对某些我想要 esbuild 能够运行的旧平台的支持(例如,旧版本的 macOS)。虽然 esbuild 的发布二进制文件是用更新版本的 Go 编译器编译的(因此在旧版本的 macOS 上不起作用),但您目前仍然能够使用 Go 1.13 为自己编译最新版本的 esbuild,并在旧版本的 macOS 上使用它,因为 esbuild 的代码仍然可以使用 Go 1.13 及更早版本进行编译。

人们和/或自动化工具有时会看到 go.mod 中的 go 1.13 行,并抱怨 esbuild 的发布二进制文件是用 Go 1.13 构建的,这是一个非常旧的 Go 版本。但是,事实并非如此。go.mod 中的该行仅指定了最低编译器版本。它与 esbuild 的发布二进制文件使用的 Go 版本无关,该版本是更新版本的 Go。 请阅读文档。

人们有时还希望 esbuild 更新 golang.org/x/sys 依赖项,因为 esbuild 使用的版本存在已知漏洞(特别是关于 Faccessat 函数的 GO-2022-0493)。阻止 esbuild 更新到更新版本的 golang.org/x/sys 依赖项的问题是,更新版本已开始使用 unsafe.Slice 函数,该函数是在 Go 1.17 中首次引入的(因此无法在旧版本的 Go 中编译)。但是,此漏洞报告无关紧要,因为 a) esbuild 根本不会调用该函数,并且 b) esbuild 是一个构建工具,而不是沙箱,esbuild 的文件系统访问不是安全敏感的。

我不会放弃对旧平台的兼容性,也不会阻止某些人使用 esbuild,仅仅是为了解决无关的漏洞报告。请忽略有关上述问题的任何报告。

压缩换行符

人们有时会惊讶地发现,esbuild 的压缩器通常会将 JavaScript 字符串中的字符转义序列 \n 更改为模板字面量中的换行符。但这是故意的。这不是 esbuild 的错误。压缩器的任务是生成尽可能紧凑的输出,该输出等效于输入。字符转义序列 \n 长度为两个字节,而换行符长度为一个字节。

例如,此代码长度为 21 字节

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

而此代码长度为 18 字节

var text=`a
b
c
`;

因此,第二段代码是完全压缩的,而第一段代码不是。压缩代码并不意味着将所有代码放在一行上。相反,压缩代码意味着生成使用尽可能少字节的等效代码。在 JavaScript 中,未标记的模板字面量等效于字符串字面量,因此 esbuild 在这里做的是正确的。

避免名称冲突

入口点模块中的顶级变量在浏览器中运行 esbuild 的输出时永远不会出现在全局范围内。如果发生这种情况,则意味着您没有遵循 esbuild 关于输出格式的文档,并且正在错误地使用 esbuild。这不是 esbuild 的错误。

具体来说,在浏览器中运行 esbuild 的输出时,您必须执行以下两种操作之一

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

    如果您在全局范围内运行代码,那么您应该使用 --format=iife。这会导致 esbuild 的输出包装您的代码,以便顶级变量在嵌套范围内声明。

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

    如果您使用 --format=esm,那么您必须将代码作为模块运行。这会导致浏览器包装您的代码,以便顶级变量在嵌套范围内声明。

使用 --format=esm<script src="..."> 将以微妙且令人困惑的方式破坏您的代码(省略 type="module" 意味着所有顶级变量都将出现在全局范围内,这将与其他 JavaScript 文件中具有相同名称的顶级变量发生冲突)。

顶级 var

人们有时会惊讶地发现,esbuild 有时会将顶级 letconstclass 声明重写为 var 声明。这样做有几个原因

请注意,esbuild 不会保留顶级 TDZ 副作用,因为模块可能需要延迟初始化(如上所述),这意味着将声明与初始化分离。顶级符号的 TDZ 检查可以通过生成额外的代码来支持,该代码在使用顶级符号之前进行检查,如果它尚未初始化,则抛出异常(有效地手动实现真正的 JavaScript VM 会做的事情)。但是,这对于代码大小和运行时来说似乎是过度的开销,并且似乎不是面向生产的捆绑器应该做的事情。