前端构建工具 2023

标准

这里定义的前端构建工具需要符合以下条件:

  • 能够构建生产用的前端资源。
  • 提供带 HMR 的 Dev Server。
  • 支持常见的前端技术,比如 Sass, LESS, CSS modules, code splitting。
  • 支持插件扩展。
  • 不限于某种 UI 框架,不捆绑不必要的运行时内容,比如 Redux。(排除 UMI 之类的深度定制框架)

生产可用

这里定义的生产可用条件:

  • 流行度高。(npm 周下载量 >100k)
  • 维护活跃。(六个月内至少发布了一个版本。)
  • 没有严重的性能或兼容性问题。

Vite 4

Vite 基于 esbuild (用 Go 语言编写) 和 Rollup。它采用了 Unbundled Dev Server 模式,通过浏览器原生 ESM 特性,省去了 Bundle 步骤,以达到比 Webpack Dev Server 更快的 HMR 和启动速度。Vite 4 提供了一个新的基于 SWC 的 React 插件 @vitejs/plugin-react-swc ,用于替换旧的基于 Babel 的插件。这让 React Fast Refresh 的速度快了 20 倍。目前,Vite 仍然是速度最快的前端构建解决方案之一。

Vite 易于使用。即使不进行任何配置,也能开始使用。如果需要定制,它的文档也非常详细好用。Vite 兼容 Rollup 插件和配置接口,甚至可以借用 Rollup 的文档。用户更容易找到需要的插件,遇到问题也更容易找到解决方案。编写 Vite 插件也相对容易。所以 Vite 的社群和生态系统成长很快。Vite 的 JS API 设计简单清晰,功能丰富。基于 Vite 开发你自己的构建工具很容易。

Parcel 2

Parcel 2 基于一系列用 Rust 重写的高性能模块,比如 SWC 和 Parcel CSS,实现了优于 Webpack 的性能。默认配置下,Parcel 2 的性能介于 Vite 4 和 Webpack 5 之间。但是可以通过启用一些实验性功能提升性能,比如 swc 压缩。

Parcel 2 的另外一个特点是开箱即用。它的默认配置已经涵盖了 TypeScript,CSS modules,JSX,React Fast Refresh,图片压缩,SVG 优化等特性支持。另外,当检测到 Sass 或 LESS 时,Parcel 2 会自动帮你安装需要的依赖包。对于常规的前端应用,你完全不需要配置文件。

Parcel 有自己独特的架构和接口设计,这让它几乎不可能直接借用 Webpack 和 Rollup 的插件。因此它的生态系统中第三方插件很少。另外,由于 Parcel 发布大版本的频率很低,内置插件为了避免 Breaking Change 而很难升级,比如 MDX 2.0 至今没有被支持。这可能是 Parcel 2 最大的劣势。

Webpack 5

Webpack 依然是最流行的选择,有最完善的特性,以及最大的生态系统。

Webpack 很慢,但是你可以用一些 loader 和插件替换默认组件来提速:

观察列表

一些还处于早期阶段的有潜力项目。

Rspack

用 Rust 写的 Webpack 替代品,由字节跳动创建。虽然还处于早期阶段,但是已经表现出比 Vite 更快的构建速度。同时兼容 Webpack 插件,可扩展性很强。

未入选

Turbopack

为 Next.js 框架设计的高性能构建工具,原生支持 SSR。未入选原因是因为目前只能用于 Next.js 框架。

UMI

蚂蚁集团的构建工具。未入选原因是因为过度定制化,只支持 React,内置了过多 Redux 之类的运行时。

ICE

阿里巴巴的构建工具。未入选原因是因为过度定制化,只支持 React,内置了过多 Redux 之类的运行时。

Create React App

React 官方的构建工具。未入选的原因是因为过度定制化,只支持 React,不支持配置和插件。

性能测试

dev server cold startdev server warm startproduction build
rspack 0.1.12.2s2.0s1.4s
vite 4.2.14.3s1.3s6.5s
parcel 2.8.313.0s1.3s15.7s
webpack 5.76.214.2s14.2s15.8s

详情参见 https://github.com/guoyunhe/front-end-builder-benchmark

技术趋势

一些可能会让前端构建工具更快更好的技术。

原生 Node.js 模块与 WASM 模块

用 Rust,Go 等语言编写的 Node.js 模块,其性能与产物格式有很大的关系。不考虑多线程的情况,从高到低分别是 Native > WASM > JS。JS 和 WASM 本身都是单线程的,除非使用 Cluster 模式(目前还没有在前端构建工具上比较成功的)。支持多线程的 Native 模块,性能会更强,esbuild 就是一个例子。

但是,原生模块必须为不同的平台预编译对应的二进制文件,不如 WASM 或 JS 的“一次编译,到处运行”方便。

在实践中,原生模块通常用于核心高负载模块,比如 rspack,swc,esbuild。而插件还是广泛采用 JS。WASM 仍然比较少见,一个例子是 swc 的插件开发。

从 Unbundled 回到 Bundled?

用 JS 写的 Webpack Dev Server 采用 Bundled 模式启动。即先由 Webpack 将多个源码文件打包成一个,再传输给浏览器。在浏览器的网络记录中,你只会看到几个网络请求。因为 Webpack 打包的效率低,启动速度很慢,热加载也很慢。

后来出来了 Snowpack (已废弃),可能是第一个 Unbundled 模式的 Dev Server。采用 Unbundled 模式启动 Dev Server,通过原生 ESM 加载,省去了 Bundle 的过程,提高了启动速度。当 Dev Server 启动时,不会进行编译,而是在接到浏览器请求时,动态编译每个文件,每次传输一个源文件对应的代码。过程大致是:

请求 → 编译 → 响应 → 请求 → 编译 → 响应 → … → 请求 → 编译 → 响应

这导致 Snowpack 的启动速度极其缓慢,对比 Webpack Dev Server 并没有显著优势。

后来 Vite 继承并改进了 Snowpack 的思路。它将 node_modules 中大量的依赖用 esbuild 先 Bundle 成单个文件,而项目源码依旧采用 Unbundled 模式。得益于 esbuild 的强大性能,不管是依赖的 Bundle,还是单个源码文件的转译,都非常快。因此 Vite 对比 Webpack Dev Server 取得了巨大的性能优势。

然而,随着项目体积增大,需要加载的 ESM 模块越来越多,Vite 的启动速度会变慢。假设项目的 src 中有 1000 个 JS/TS 文件,就会有 1000 个 HTTP 请求。现代的浏览器只允许每个服务器建立 6 个连接,也就是说最多只能并发 6 个 HTTP 请求,导致阻塞。

因此 Parcel 2,Turbopack 和 Rspack 选择了回到 Webpack Dev Server 的 Bundled 模式,通过用 Rust 重写关键部件,改进打包和编译性能来提高性能。

但是根据 Turbopack 的 Benchmark,只有在 src 中有 1000 个以上的 React 组件时,Turbopack 对比 Vite 才会有比较明显的优势。但实际应用中,1000 个组件(约100K到1M行代码)已经是相当大规模的应用了。而 Vite 的冷启动速度仍然可以接受:4.2s。另外,我觉得这个 Benchmark 用 Turobopack 的 Server Side Rendering 对比 Vite 的 Client Side Rendering,并不公平,因为 Vite 也是支持 Server Side Rendering 的。

总结一下,我觉得 Vite 的 Unbundled 策略和 Parcel 等的 Bundled 策略各有所长,在实际应用中并不能拉开明显的性能差距,未来很长一段时间仍然会共存,而不是此消彼长。

更快的 Sass 编译器

Sass 的老编译器 node-sass 已经被官方废弃,不再维护。新的官方编译器 Dart Sass 令人迷惑:

  • 原生构建二进制程序没有 Node.js 接口,无法集成到现有的 Node.js 工具中。除非你用 Node.js 的 exec 函数执行。(不推荐在生产中这样做)
  • JavaScript 构建要比 node-sass 慢 4 到 30 倍 (取决于项目大小)。如果你的构建工具调用 Sass 接口的方式不正确,速度甚至会更慢。详见 Issue #1557#1534

对于大量使用 Sass 的生产项目,这肯定是不可接受的。很多项目选择继续使用废弃的 node-sass 编译器。

现在已经有人在开发用 Rust 重写的 Sass 编译器:

  • grass。只提供编译成 WASM 的版本,但是性能并不好。
  • rsass。可惜,这个项目也没支持 Node.js API,也无法直接用于现在的前端构建工具生态。

更快的 TypeScript 检查器

TypeScript 很慢,因为它完全是 JavaScript 写的。幸运的是,我们通常不需要在构建前端项目的时候检查类型。Babel, esbuild 或 swc 会直接忽略所有类型。但是,如果你项目有很严格的 QA 要求,你不得不运行 tsc 命令检查类型,CI 工作流就会被拖慢,即使对于小型项目 (10k lines) 或中型项目 (100k lines) 也很明显。

于是乎,重写 TypeScript 检查器的呼声越来越高。我们已经能看到一些这方面的尝试:

前端构建工具测评

2022-04-02 更新:增加测评 JS Compiler 的测试数据

2022-03-21 更新:增加测评 Bundler 和 JS Minifier 的测试数据


前端构建工具已经形成了一个或多个复杂的生态系统。从最基础的 webpack, rollup 和 babel, esbuild, swc 等工具库,衍生出各种各样的插件,封装组合成 crate-react-app, vite, parcel, snowpack 这样通用解决方案,进而发展出 Next.js, ICE.js, UMI.js 这种特异化的行业解决方案。

这个生态中的每个部分,都有其替代品。比如 swc 可以替代 babel, rollup 可以替代 webpack。swc 确实比 babel 快得多,但是插件生态不如 babel 全面,两者互有胜负。项目的实际需求,决定了构建工具的选型。

随着前端工程越来越大,构建性能和热加载性能逐渐下降,大型项目不得不面临两个选择:

  1. 对仓库/应用进行拆分,采用微前端等复杂技术整合多个子应用。这样固然能够减少每次发布构建的时间,但是会增加开发人力开销。由于影响了项目架构,严重依赖人工,无法大批量复用到多个项目。
  2. 提升构建工具的性能,比如用 esbuild 这种原生程序替代 babel 这种 Node 程序,用 Vite 这种 Bundleless 工具替代 webpack-dev-server。相比迁移项目架构,改造构建工具的成本更低,完全可以通过迁移脚本自动化。

这篇文章会介绍和评测当前各种流行的构建工具,从底层库到完整解决方案,帮助大家选择和定制最适合自己的方案。

Bundler

Bundler 是能够通过 import/export 将很多 JavaScript 文件,打包成一个/几个文件的工具。在前端开发中,也经常会 Bundle 其他文件类型,比如 CSS/LESS/SCSS,以及图片和字体。这些部分通常是以 Bundler 插件的形式提供。因此 Bundler 通常是一个平台,可以通过插件来扩展特性。

Webpack 是最流行的 Bundler。Rollup,Parcel 则是后起之秀。以编译器见长的 esbuild 和 swc 也提供了 Bundle 能力,但是插件生态不足。

特性比较

spack 还处在实验阶段,功能缺失非常多,因此不建议在任何实际项目中使用。

esbuild 稍好一些,如果你不需要 CSS Modules,不用代码分割,不需要支持 IE,也不需要输出 UMD,那么 esbuild 是可以满足你的需求的。 对于个人项目来说,这完全是可行的。对于企业项目,esbuild 作为 bundler 显然还不够全面,但它作为编译器和 Rollup 配合使用还是很好用的。

parcel,rollup 和 webpack 是功能最为全面的,能够应对绝大多数前端项目需求。webpack 的插件生态最丰富,兼容性也最好。rollup 和 parcel 则更加轻量化。Parcel 的 UMD 打包格式缺失,不适合用于组件库或微前端构建,但是对普通应用还是够用的。

webpackrollupparcelesbuildspack
CSS Modules✔️✔️✔️✔️
LESS✔️✔️✔️✔️
Sass✔️✔️✔️✔️
代码分割✔️✔️✔️
CJS 输出格式✔️✔️✔️✔️✔️
ESM 输出格式✔️✔️✔️✔️✔️
UMD 输出格式✔️✔️

性能比较

测试打包 antd + lodash + react + react-dom + three.js 的速度。为了控制变量,统一采用 esbuild 作为 minifier(spack 不支持除外)。

详细测试数据见 https://github.com/guoyunhe/benchmark-bundlers

webpackrollupparcelesbuildspack
ThinkPad T4805.347s14.548s6.069s0.249s1.082s
MacBook 16, 20194.229s12.790s5.225s0.227s1.012s

esbuild 是用 Go 语言编写,spack(swcpack)是用 Rust 语言编写。两者都编译成二进制文件运行,速度比其他 JS 编写的 Bundler 快几十倍。从 20s 到 0.2s 已经是一个质的飞跃了。

相比之下,传统的 JS Bundler 就慢得多了。令人意外的是,被很多人认为过时了的 webpack 居然比 parcel 和 rollup 更快。尤其是 rollup 的性能,令人意外地差。我们分析是因为 lodash 和 react 都是 CJS 模块,rollup 需要将其转化成 ESM 才能使用,因此比 webpack 慢了一倍多。

综合评价

目前 webpack 仍然是最全面的 Bundler 选择,支持特性最丰富,生态最为庞大。而且它的性能并不差,在应用打包场景要优于 rollup 和 parcel。

而放眼未来,esbuild 的潜力最大。它的性能无与伦比,且具有成熟的插件接口。它的代码分割能力已经在测试中,前途一片光明。甚至 Vite 也希望在 esbuild 成熟之后,移除 rollup 而直接用 esbuild 打包。

JS Minifier

Production 构建需要对 JS 文件和 CSS 文件进行 Minify 处理,让生成的文件更小,加载更快。JS 的 Minifier 包括 terser,esbuild,swc。衡量它们优劣的主要指标是速度和压缩率。

以下测试用 webpack + terser/esbuild/swc 打包和压缩 antd + lodash + react + react-dom+three.js。详情见 https://github.com/guoyunhe/benchmark-js-minifiers

no-minifyterseresbuildswc
产物尺寸4.7MB1.8MB1.9MB1.9MB
运行时间 ThinkPad T4804.677s17.046s5.615s8.997s
运行时间 MacBook 16, 20193.556s13.834s4.052s6.892s

esbuild 和 swc 在速度上大幅度领先 terser。对于这类高计算量任务,Node.js 的性能比原生程序差得多。同为原生程序,esbuild 比 swc 的性能更好,说明它的设计更优秀。而从压缩率上来看,terser 略好于 esbuild 和 swc 但差距不大。因此综合来看,我认为 esbuild 是最佳的 minifier。

JS/TS Compiler

我们在开发中喜欢使用最新的 ES 语法,但在发布时需要兼容旧的浏览器。JSX 和 Vue 文件还包含了特殊语法,即使最新的浏览器也无法识别和运行。因此越来越多的前端项目需要用 JS Compiler 或者叫 Transpiler 来转换源码。这也是一项非常耗时的任务。目前最广泛使用的编译器是 babel,它功能全面,但性能堪忧。而 esbuild 和 swc 则是以速度见长的面向未来的编译器。TypeScript 有自己的编译器 tsc,支持 TypeScript 的类型系统检查,但是它的效率极差。因此 Babel, esbuild 和 swc 都提供了自己对 TypeScript 转译的支持。

特性比较

从特性上来看,babel 仍然是最全面的。esbuild 和 swc 则注重对最新 ES 标准的支持,因此少了对非标准的 JS 修饰器语法(但是支持 TS 修饰器语法)和 Flow 语言的支持。esbuild 不兼容 ES5 和 IE 浏览器,但某些项目仍然有这方面的需求,开发者需要平衡利弊。

babelesbuildswc
ES Next
ES5, IE11
JS 修饰器语法
Flow 语言
TypeScript 语言
TS 修饰器语法
JSX

性能比较

以下测试用 Webpack + babel-loader/swc-loader/esbuild-loader 转译 antd + three.js 源码到 ES5 语法,不进行 minify。详见

babelesbuildswc
运行时间 ThinkPad T48010.133s0.464s0.549s
运行时间 MacBook 16, 2019

综合评价

如果不需要支持 IE11,那么 esbuild 是最好的选择,它的性能非常好。

如果需要支持 IE11,那么推荐 swc,性能接近 esbuild 且浏览器兼容性更好。

只有当 esbuild 和 swc 都无法支持你的项目需求时(比如用到了 JS 修饰器语法),再考虑 babel。babel 虽然功能最为全面,但性能比 esbuild 和 swc 慢几十倍,已经无法支持大型项目了。

Dev Servers

Webpack Dev Server

Webpack Dev Server 的出现,极大提升了 React 应用开发的效率:修改代码时,组件会自动被替换,无需手动刷新,也不会重新加载页面。然而早期的 Webpack 和 Webpack Dev Server 每次修改文件都会重新编译打包,给人越来越慢,又无能为力的痛苦感受。Webpack 5 和 Webpack Dev Server 4 已经在性能方面有了极大的改善。中小型项目的瓶颈大多不是 Webpack 本身造成,而是上面介绍过的 Compiler 和 Minifier。如果结合 swc 和 esbuild,Webpack Dev Server 仍然很能打。

但是在大型应用场景,Webpack Dev Server 还是有不足的。想象一下,你有一个包含几万行代码的 SPA 应用,且没有做代码分割。Webpack Dev Server 会将所有代码编译打包之后才能启动。这样一来,启动速度就会变得很慢。但实际上,我们每次只开发项目中很小的一部分,余下的页面/弹窗等组件并不需要提前编译加载。

但是这也并不是我们一定要抛弃 Webpack Dev Server 理由。大型应用场景,也极少硬着头皮上 SPA,而是用微前端框架将应用做进一步的切分。开发时 Webpack Dev Server 只需要运行子应用即可。

但是如果我们就是要做巨石 SPA 应用呢?在本地开发场景下,可以利用浏览器原生的 ES Module 能力动态地按需加载模块,而不需要编译打包所有内容。这样是不是就可以解决我们上面遇到的那个问题了呢?理论上行得通,但实际效果不尽如人意。

Snowpack

Snowpack 就是这样一个设计。理论上动态加载本地 JS 文件几乎没有延迟。但是这些模块并不都能直接在浏览器中运行。比如有很多 NPM 依赖是 CJS 而非 ESM 模块,我们写的代码有 JSX 和 TypeScript,更不要说还有 Sass,LESS,CSS Modules 这些要处理。因此动态加载每个文件时,都有一定的等待时间,而且无法并行处理。结果是在冷启动过程奇慢。在中小型项目开发中,不及 Webpack Dev Server 有效率。大型项目。而且,它并不能构建部署生产环境,仍然需要使用 Webpack 或 Rollup 来补足,无疑增加了项目维护成本。

因此在实际项目中,很难看到 Snowpack 的身影。Snowpack 本身的开发也陷入了很长时间的停滞不前。不过,Snowpack 的思路被 Vite 和 Parcel 2 所继承并改进,取得了成功。

Vite

Vite 给出了更好的答案:将依赖模块预先用 esbuild 进行 Bundle 以快速加载,项目模块采用 Unbundle 模式动态编译加载。

但是,Vite 对不同前端框架的支持程度是不同的,实际运行效率也差别很大。它的 Vue 插件用 esbuild 编译,速度极快。而它的 React 插件用的是 Babel 编译,速度一般。

另外我们发现,Vite 的缓存校验机制存在比较大的问题。更新 node_modules 中的依赖之后,Vite 缓存并没有更新。在开发过程中只能禁用缓存,这导致开发效率进一步降低。

Parcel 2

Parcel 2 和 Vite 的思路类似,但是有两个重要改进:

  • 缓存范围更广,效率极高,热启动在 1s 内
  • 采用了 swc 而非 babel 编译,构建和刷新速度更快

这两个优势使得 Parcel 2 在几乎各个性能指标上都领先于其他竞争对手。

性能比较

TODO