前端构建工具测评

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

TypeScript 枚举 Enum

JavaScript 本身是没有枚举的,Enum 是 TypeScript 带来的特性。

虽然早在 TypeScript 2.4 就已经提供了 Enum 但是并没有被广泛使用。TypeScript 一开始就有 Union Type,且语法更简单。因此 Enum 在 TypeScript 并不是必需的,出现较晚,且语法更加复杂,阻碍了它的流行。

但是,在大型项目中,Enum 有很多不可替代的优势,结合编辑器和IDE的功能,让类型的使用和维护更加轻松。

和 Union Type 比较

在 Enum 诞生之前,我们经常使用 Union Type 来表示枚举类型:

type Role = 'superadmin' | 'admin' | 'editor' | 'contributor';

const myRole: Role = 'editor';

如果改写成 Enum 的话:

enum Role {
  SuperAdmin = 'superadmin',
  Admin = 'admin',
  Editor = 'editor',
  Contributor = 'contributor',
}

const myRole = Role.Editor;

乍一看是不是代码繁琐了很多?那为什么我们还需要用 Enum 呢?Enum 和 Union Type 相比有两个优点:

  1. 可以对每个枚举成员添加注释
  2. 可以快速且精准地修改值和重命名

项目越大,类型也就会越多,相应的术语也就会越多。对每个枚举值成员添加注释,能够帮助新加入的开发者快速熟悉项目。尤其是国内的项目,中文的项目文档和英文代码之间的联系,注释必不可少。

/** 用户角色 */
enum Role {
  /** 超级管理员,可以增删管理员,配置和删除项目 */
  SuperAdmin = 'superadmin',
  /** 管理员,可以管理除了管理员外的成员角色 */
  Admin = 'admin',
  /** 编辑,可以直接编辑内容 */
  Editor = 'editor',
  /** 贡献者,可以参与讨论和提交补丁 */
  Contributor = 'contributor',
}

const myRole = Role.Editor;

当我们需要修改枚举值时,比如将 superadmin 改为 owner,Enum 就会非常简单。只需要将枚举值更改,而不需要去更改其他引用的地方。然而对于 Union Type,我们要小心翼翼地查找和替换每个出现的地方。加入我们想把 SuperAdmin 替换成 Owner,也只需要用 IDE 和编辑器提供的重命名/重构功能,快速而精确地重命名。这种维护的便利,也是 Enum 最大的魅力。

墓地

在大山的深处,有一个与外界少有往来的村庄,村民们过着自给自足的生活。一天,落难的传教士们在逃亡时误入此地。传教士们不擅长劳作,却会讲故事。他们编造了一个谎言,称村民们若不捐银钱求一块被祝福过的墓地,便会在死后受尽无数的折磨,灵魂不得安宁。村民们自此更加拼命劳作,开垦更多的土地,种植更多的粮食。即使如此,也少有人能捐得起一块墓地。久而久之,越来越多的人因辛劳而早亡。墓地越来越满,价格也水涨船高。人们开始互相抢夺和厮杀,而传教士们满载着钱财和粮食离开了。

单元测试:从 Jest 到 Karma+Mocha+Chai

我所参与过的 Web 项目大部分都使用 React 框架。Jest 是我们曾经最主要使用的单元测试 (Unit Test) 框架。Jest 配置足够简单,功能也足够丰富。但是随着前端开发逐渐进入深水区,Jest 的短板开始逐渐显现。我在经过探索和尝试后,迁移到了 Karma+Mocha+Chai 的解决方案。在此分享这两种技术方案的区别和取舍,希望对遇到同样问题的同学有所帮助。

继续阅读 →

前端必学&勿学清单(2022更新)

前端技术发展很快,有些技术如常青树,有些则已日渐式微。不断涌现的新技术,有的是真创新,有的是换汤不换药。本文希望帮助大家避免在没有学习价值的技术上浪费事件。

需要注意的是,仅凭个人经历去判断一项技术是不是不流行,是很片面的。很多人在整个职业生涯中都没有遇到过 Angular 项目,但这并不代表 Angular 没有人用。有人觉得 Vue 小众,是玩具,但是其生态仍然在蓬勃发展。本文尽量从技术架构和统计数据来分析,而非个人经验。

更新于 2022 年二月。

继续阅读 →

编程英语基础

编程是一门语言艺术。要写出赏心悦目的代码,最重要的便是恰当的命名变量,函数,类和包等。每种编程语言都自己独特的语法和代码风格。比如 C 语言常用 snake_case 命名,而 Java 语言用 camelCase 命名。这篇文章将不去探讨具体语言的特有规范,而是去介绍通用的命名的英语语法,比如变量类型与词性的关系,词组和短语的顺序,省略的用法,近义词的辨析等。示例以 Java/TypeScript 为主。正确示例标记为✔️,错误示例标记为❌。

本文有待完善,欢迎读者意见。

继续阅读 →

GODZILLA

哥斯拉系列的漫画,动画,电影已经非常多了。最开始喜欢哥斯拉只是单纯觉得撕裂夜空的原子吐息非常酷。最近几年重启的哥斯拉电影,在这些粉丝关注的点上还原得非常成功,但实质上并没有什么思考深度。庵野秀明的新哥斯拉,虽然被赋予了很多意义,但是呈现效果拉跨,挂羊头卖狗肉。让我印象最深刻的,反而是一部非常规的作品,虚渊玄和Netflix合作的哥斯拉动画电影三部曲:《哥斯拉:怪兽行星》,《哥斯拉:决战机械都市》,《哥斯拉:噬星者》。

继续阅读 →

夜归人

不论几时下班,总有同路人拖着同样疲惫的壳,歪歪扭扭地走着夜路。

结伴的人欢快地聊着天,直到在地铁口熟练地挥别。穿正装的人顿了一顿,点上一支烟,吸了两口便丢了,匆匆地走了。路边卖唱的母女在保安的注视下离开,笨重的音箱是无法抛弃的负担。大哥的断腿兀地横在路上,念着哦弥陀佛。卖红薯的异乡人,受夜色的庇护而未被驱赶。电瓶车像家乡的燕子一样灵巧,极限地滑过身旁,又倏然消失在还巢的鸟群里。

街道的灯光,污染了夜的纯净。

九点钟,路上游荡的身影依旧不见少。打工人三五成群,倚靠在电瓶车上闲谈打趣。寡言少语的人,吃完一盘炒粉,还坐在那里等着。超市店员叫卖着品相欠佳的柑桔,似在等换班。大姐们的广告位争夺战仍在继续。每次贴完租房广告,都会盯一会儿,免得被人覆盖。那种靠眼神就能判断对方贴的是哪张的炉火纯青,是我辈不曾到达的境界。

在附近住了一月有余,却记不起一个熟面孔。大概我也是不会有人注意到的那个。昏暗的走廊,布满锈迹和灰尘的铁门,狭小的居室,无暇拆开的行李。不去想太多的话,在陌生的城市生活也非难事。不去喜欢,不去讨厌,不去刻意了解它的一切。

感谢这个暂时的容身之所,让我还有力气幻想远方。

二十九岁生日记

这天只是普通地上着班,做着与平时无异的工作。想订个生日蛋糕,但并不喜欢吃。于是什么都没有准备。倒是老家三代人买了个生日蛋糕,远程庆祝了一番。外甥女这天学会了走路。

最近并不顺利呢,辗转反侧。似有什么带走了夏天温度,吸走了空气中稀薄的氧气。世界繁华热闹的背景音渐渐隐去,每一丝低声啜泣都汹涌而来。那些非常不好的事情,找不到人娓娓道来。如果说重来一次可以做到更好,便有责怪自己的理由。却好像每一步都已经用尽全力,怕得要死的时候,也还是埋头前进。

南方的热终于过去了,少了楼下摊档的吵闹,狭小的房间似乎也宽敞了一些。不甚和睦的邻居,也放下了争执。简单煮了一碗面,从此这里算是一个正式的落脚点了。

老人形容新人,总会用一个词:心气。不安于现状,心心念念着摇摇欲坠的理想乡。高谈阔论,热衷表达,沉浸于自我。这些略带贬义的描述,对我这样的人恰如其分。

要勇敢呀。