esbuild 二进制下载提速 3 倍的秘密

Posted by Lxxyx on 2021-09-29

image.png

esbuild 原有下载机制的问题

最近有用户在 esbuild 的 Github 仓库提了一个 Issue:Different strategy for installing platform-specific binaries,报告了部分用户在安装 esbuild 依赖时出现的权限错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> esbuild@0.8.43 postinstall /usr/local/lib/node_modules/netlify-cli/node_modules/esbuild
> node install.js
Trying to install "esbuild-linux-64" using npm
Failed to install "esbuild-linux-64" using npm: EACCES: permission denied, mkdir '/usr/local/lib/node_modules/netlify-cli/node_modules/esbuild/esbuild-r63cli2pgpi'
Trying to download "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.8.43.tgz"
/usr/local/lib/node_modules/netlify-cli/node_modules/esbuild/install.js:217
throw e;
^
Error: EACCES: permission denied, open '/usr/local/lib/node_modules/netlify-cli/node_modules/esbuild/bin/esbuild'
errno: -13,
syscall: 'open',
code: 'EACCES',
path: '/usr/local/lib/node_modules/netlify-cli/node_modules/esbuild/bin/esbuild'
}

背后的原因也很简单,因为 esbuild 是使用 Go 语言开发的,因此如果要使用 esbuild ,那么在安装时就必须下载各自平台对应的二进制包。

而在之前的 esbuild 版本中,这段逻辑是利用 npm postinstall 的 hooks 去实现的,在安装时会通过 node 判断系统平台,并下载对应的二进制包。

这样做也带来一些问题:

  • 权限问题,动态下载可能会报错
  • 二进制包是从 npm 下载的,不支持内网、自定义 npm 源、代理等情况,速度也慢

在 Issue 中,也提到了可以使用 npm 的 optionalDependencies + cpu 来解决多平台二进制包分发的难题。

随后 esbuild 实现了该逻辑,并提交了 PR。

image.png

测试数据的结果也非常喜人,安装时间从 5.4s 减少到了 1.5s,提速了大约 3 倍。
image.png

在仔细查看相关 PR 后,我觉得这是一个非常巧妙的解决方案。

npm 的 optionalDependencies 与 cpu

长话短说。

在 npm 中,optionalDependencies 意味着是可选依赖,此类依赖在安装时,即使是安装失败了也不会中断安装的过程,而是交由 npm 包自行去判断。

而 cpu 字段则代表这个 npm 包只能在指定的 cpu 架构上运行。比如你指定了"cpu": [ "arm" ],那么 npm 在安装时会通过 process.arch 来判断,这个包是否可以安装。

而 esbuild,正是通过 optionalDependenciescpu 字段的配合,实现了下载提速 3 倍的效果。

下载提速的原理

在 PR 中,esbuild 将所有的二进制包都加入了 optionalDependencies 中。

image.png
而同时在每个二进制包的 package.json 中,指定了 cpu 字段,也就是将原来的匹配平台,下载对应二进制包的工作交由了 npm 去实现。

尝试通过 npm i esbuild --verbose 来输出日志,可以清晰的看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
npm verb reify failed optional dependency esbuild/node_modules/esbuild-windows-arm64
npm verb reify failed optional dependency esbuild/node_modules/esbuild-windows-64
npm verb reify failed optional dependency esbuild/node_modules/esbuild-windows-32
npm verb reify failed optional dependency esbuild/node_modules/esbuild-sunos-64
npm verb reify failed optional dependency esbuild/node_modules/esbuild-openbsd-64
npm verb reify failed optional dependency esbuild/node_modules/esbuild-linux-ppc64le
npm verb reify failed optional dependency esbuild/node_modules/esbuild-linux-mips64le
npm verb reify failed optional dependency esbuild/node_modules/esbuild-linux-arm64
npm verb reify failed optional dependency esbuild/node_modules/esbuild-linux-arm
npm verb reify failed optional dependency esbuild/node_modules/esbuild-linux-64
npm verb reify failed optional dependency esbuild/node_modules/esbuild-linux-32
npm verb reify failed optional dependency esbuild/node_modules/esbuild-freebsd-arm64
npm verb reify failed optional dependency esbuild/node_modules/esbuild-freebsd-64
npm verb reify failed optional dependency esbuild/node_modules/esbuild-darwin-64
npm verb reify failed optional dependency esbuild/node_modules/esbuild-android-arm64

npm http fetch GET 200 https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.3.tgz 3635ms (cache miss)
npm timing reifyNode:node_modules/esbuild-darwin-arm64 Completed in 3641ms

因为我的系统是 m1 的 mac,因此只有 esbuild-darwin-arm64 这个包被安装了下来,其他的包则失败了。

这么做的好处在于:

  • 不用动态去 npm 拉取包,对于使用自定义 npm 源的用户是友好的(特别是使用 npm 镜像源做反代的情况)
  • 安装全程由 npm 完成,最大程度的避免了权限的问题

原理很简单,但是思路很巧妙,值得学习。