前端构建工具 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 检查器的呼声越来越高。我们已经能看到一些这方面的尝试:

前端最佳实践:字体

字体单位

px 依旧是最全面的选择,尤其是重交互的网站:

  1. 能够做到设计稿的像素级还原。
  2. 用户体验统一,变量少,可控。
  3. 方便配合 JS 动态计算布局,JS 接口通常只能获取像素为单位的尺寸和位置。
  4. icon,图片和 border 通常都是使用像素尺寸,字体采用像素单位更容易配合。

rem 在文字内容型网站上更加灵活,适合新闻,博客等:

  1. 自适应不同设备,不同用户偏好,获得最佳阅读体验。
  2. 无需手写繁复的 media query。

有些创意内容网站使用 vw 尺寸系统,字体和图片总是同窗口比例缩放,以实现类似海报布局的整体感,但也仅限于这一类型的网站。

em 和 % 会用在局部使用,但存在嵌套后尺寸不容易控制的问题,极少在项目中大范围使用。

pt 是印刷常用单位,不适合现如今的 Web 环境,且和 px 对应关系复杂,应该避免。

正文字体大小

12px 是 Chrome 浏览器默认支持显示的最小字体。即使 CSS 设置了 9px,最终用户看到的仍然是 12px 字体。因此在设计中使用小于 12px 的字体是一个严重的错误。12px 的英文字体可读性尚可,但是中文可读性比较差。建议作为次要文本字体的尺寸。

13px 是信息密度较高,中文可读性尚可的选择。比如 Facebook 和百度用的就是 13px。

14px 是信息密度和中文可读性的一个比较好的平衡点,适合界面复杂且空间比较紧张的网站的正文字体大小。比如

16px 是大部分浏览器的默认字体大小,可读性好,但是信息密度不高,适合一般网站。

18px 以上通常只有在创意营销网站上才会作为正文字体大小使用。

一个比较通用的策略是:

  • 12px:次要文字,页脚链接,标签分类,面包屑,输入框提示,小按钮,小输入框
  • 14px:普通文字,正文,输入框,按钮
  • 16px:突出文字,卡片标题,大按钮,搜索栏

标题层级字体大小

HTML 支持 H1 到 H6 共六级标题。但实际应用中,我们应当控制标题的层级数量,层级越多,使用体验会越差。

功能为主的网站对标题依赖比较小,更多是靠 Card 和 Tab 等容器进行界面层级管理。

内容型网站对标题层级的需求会更高一些,但也尽量不超过三级。

H1 字体大小取决于网站类型对信息密度的要求。以功能为主的系统通常 H1 会比较小,不会超过 40px。

H1, H2, H3 之间应当有 30% 左右的递减,视觉上差异才够显著。比如:

  • H1, 36px:页面标题
  • H2, 24px:章节标题
  • H3, 16px:子章节标题,卡片标题,弹窗标题

字重

最早字体只支持两种字重,正常(normal, 400)和粗体(bold, 700)。现代 Web 字体的字重支持 100 到 900 等 9 种字重,100 最细,900 最粗。但是系统字体支持的字重数量通常较少。

Windows 默认中文字体“雅黑”支持 300, 400 和 700 三种字重。macOS 和 iOS 默认中文字体“苹方”支持 100/200/300/400/500/600 六种字重(苹果更喜欢使用较细的字体设计)。也就是说“苹方”的粗体,会比“雅黑”要细一些。Android 系统默认字体 Noto Sans CJK 支持 200/300/400/500/700/900 六种字重,最为全面。部分 Android 系统,比如魅族 Flyme 和小米 MIUI,甚至搞出了字重的无极调节。

由于中文字体体量巨大,通常网站不会加载远端中文字体,而是使用系统字体。因此,在使用字重的时候,就需要考虑系统字体的字重支持。综合主流操作系统,得出兼容性最好的字重集合:细体 300,正常 400,粗体 700/600

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

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

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

更新于 2022 年二月。

继续阅读 →

position: fix 没用?你大概是被 transform 坑了

在 CSS 里很多位置布局都是相对于容器的。但是 position: fixed; 比较直白,只相对于窗口,通常不会被干扰。但是我最近就遇到了这么个问题:

transform:scale(1);
position:fixed; right:10px; bottom:10px;

这不对呀,我明明写的是 position: fixed; 为啥没用呢?

于是我一个一个翻看父元素的 CSS 布局属性,发现一个 Modal 库有个奇怪的 transform: scale(1); 。这个属性没有任何视觉的效果,显然是动画执行之后留下的。然而在技术实现上,即使是 scale(1)translateX(0) 这种没有任何效果的 transform ,也会重建一个坐标系,导致内部元素的 position:fixed 不再相对于窗口,而是相对于这个 transform 元素。

这是不是某个浏览器实现的缺陷呢?实际测试 Firefox 和 Chrome 都是一样的效果。这也许是一个 Web 标准中比较含糊的灰色区域。

解决方法比较简单粗暴,把 transform: scale(1) 改成 transform: unset 即可。在实践中,尽量避免对比较大的容器使用 transform,比如 Sidebar 和 Modal。如果要用的话,则要确保子元素不会用到 position: fixed,比如一些 Popup。