分享:Rust 构建工具在蚂蚁的研发和落地

注:
1、由于本文内容报名了今年 5 月的 GIAC 会议,所以很抱歉,本文档不能在会后分享给大家。

Hello,大家好。今天和大家分享的是《Rust 构建工具在蚂蚁的研发和落地》。这个工具名叫 Mako ['mɑːkoʊ],意为「最快的鱼」。我们做了一年,在内部已落地几百个应用,今天和大家分享下期间遇到的问题和收获的经验。

个人介绍。

陈成,花名云谦,Github 账号为 sorrycc,阿里 & 蚂蚁 15 年+老前端,是 Umi、Dva、babel-plugin-import 等开源库的作者,目前在蚂蚁集团负责中台前端框架 Bigfish 和基于 Rust 构建工具 Mako 的开发工作。此外,还有维护了发布了 100 多期的「MDH 前端周刊」和发布了 400 多篇文章的「前端日更知识星球」。个人在 React、框架开发、性能优化及前端工程化等领域相对比较专业,期待和您的交流。

我们做 Mako 经历了 3 个阶段,从 0 到 1(10m)、从 1 到落地(15m)、从落地到开源(5m)。今天就从这 3 个时间段和大家拆开了分享下。


从 0 到 1

Mako 的缘起要回到去年(2023 年)3 月,那会老板成立「体验科技小队」,立了 3 个项,分别是 Rust、SSR & RSC、AIGC。我领了 Rust 的部分,洋洋洒洒地写了个 RFC,《[[基于 Rust 的新前端研发态:RFC]]》。

现状是,1)《Rust 是 JavaScript 基建的未来》,前端社区的工具大多已经或将要用 Rust 重写,蚂蚁不进则退,2)我们有很多基于 webpack 的构建提速方案,但治标不治本。所以,我们需要用 Rust 询求构建速度问题的彻底解,同时这也会给我们带来更多可能性

此外,构建工具是第一个里程碑,我们的目标可能更大一些,期望整个框架都基于 Rust 重写。因为现在的框架都是元框架,除了构建,框架中还有大量编译相关的任务,只解构建可能对于开发者用户来说,体感没那么强。

但是,回到现实。我们知道了想要做成什么样(What),现在需要解 How 的问题,过程中就面临着很多问题。

  • 意义感,社区已经有了,我们为什么做
  • 人员有限,框架团队仅 3 人,现有工作也不能荒废
  • 团队全部 Rust 0 基础
  • 从 webpack 配置工程师到 webpack 研发工程师的角色转变
  • Less 由于 antd 的原因,内网大量应用,而 Less 无 Native 实现

第一步就是解 Why 的问题。社区已经有了 esbuild、turbopack、farm、rspack、swc-bundler(、rolldown)之后,为什么我们还要重头写一个?原因是复杂的,当你想写一个的时候,总是能找到很多理由。1)社区库的成熟度和需求匹配度(展开下…) 365 – 《Mako 开发日志(5) – Why Mako》,2)主动权,业务原因,我们会有大量定制需求,3)元框架是编译时框架,和构建耦合很深,4)我想(带)学(薪) Rust。

Farm 开发人员少,没大厂背书,落地少 edge case 会是个问题;Rspack 不喜欢其兼容 Webpack 的设计,兼容意味着权衡和潜在的性能消耗,既然重新用 Rust 写了,不如就更彻底一些;Turbopack 是最想和他摩擦出火花的一个,但无奈太封闭,商业公司的开源商业气息有点重,Next.js only,且当时只有 dev,同时也是我们太菜,看了好久他的代码没看懂。
365 – 《Mako 开发日志(5) – Why Mako》

第二步是解 Who 的问题。1)实线 + 虚线团队,虚线团队有好有坏,好处是补充人力,坏处是不稳定,业务有事就撤,所以 assign 大功能时需谨慎,撤了再找人顶上成本太大,2)Github 私库开发,优点是 Github 体验好,缺点是私库 CI/CD 的 Action 收费太贵,3)0 基础如何快速上手 Rust?个人的经验是 B 站视频快速打底、看电子书系统学习和深入、多实践、然后不懂问 AI(或 Mako 团队同学)。

302 – 《Rust 资料》
7 – 《资源整理:前端视角学 Rust》
364 – 《作为前端开发者,到底有没有必要学 Rust》

除了 Rust 的知识需要补充,我们还需要补充构建相关的知识。1)对于这个问题的功能层面,tooling.report 有比较全面的答案,我整理了 280 – 《构建工具应该包含哪些能力》。2)同时,我的习惯是万事从 Toy Version 开始,所以照例先写了个 Toy Bundler 来做下想法验证。(注:基于 JavaScript,那会还不会 Rust)。3)再有,就是像竞品学习,包括 Farm、Webpack、Rspack 和 Turbopack 等,前两个偏多一些。

用 Rust 实现构建器的流程如下(esbuild 的架构图)。

接着就是日复一日的研发阶段。调研、学 Rust、开发、修 Bug、提升性能,接着调研、学 Rust、开发…,基本上就是这样的循环。不得不说,Rust 的门槛还是挺高的,刚开始时,用 Rust 开发的速度可能只有用 JavaScript 的 1/10 到 1/5,随着深入,目前已达 2/3 到 3/4。

Mako x Less,内容太多了,可以先略。
木桶原理
352 – 《Mako 开发日志(4):Less》
[[难题:Rust 版 Less]]

2023.07.07,经过几个月的开发,Mako 发布了 Alpha。包含了构建工具所应该有的大部分功能,比如 resolve、load、transform、parse、generate 等,支持 css、js、assets 等资源文件,支持 hmr、mimizing、source map、alias、externals、public_path、targets、css modules、svgr、node 补丁等功能。


从 1 到落地

对于 Mako 来说,从 0 到 1 做出来是简单的,从 1 到落地真实项目则是复杂的。我们要想把 Mako 大规模落地到蚂蚁的项目中,还需要解很多问题。

  • Edge Case(边界场景),真实世界是复杂的
  • 构建性能,快!快!快!
  • 产物性能,不仅尺寸小,加载速度还要快
  • DX(研发体验),错误处理、HMR 等

用「工程化」思路解 Edge Case 问题。蚂蚁的一个优势是,我们有大量(长千上万)的存量项目,这些项目基本上能覆盖我们遇到过的所有 Edge Case,同时大家的技术栈是统一的,用了相同的框架。所以,我们可以收集这些项目,以及这些项目所使用的依赖,在 Mako 发布之前,把这些全部跑一遍,以确保能提前覆盖各种 Edge Case。

e.g. CSS 的奇怪用法(from 358 – 《CSS 错误片段集锦》

@media screen (max-width: 800px) {
    .jsonWrapper {
        width: 300px;
        overflow-y: auto;
    }
}

e.g. JS 的奇怪用法(循环依赖、esm/cjs 混用、webpack 特有语法、…)。

// esm / cjs 混用
import a from 'a';
module.exports = a;

// webpack 特有语法
__webpack_public_path__
__webpack_require__
__webpack_chunk_load__
__webpack_module__
...

// require with expression
require('./i18n/' + selectedLang);

// try require
try {
  require('an-unresolvable-dep');
} catch(e) {}

// falsy require
if (typeof require === 'undefined') {
  require('fs').readFileSync('abc', 'utf-8');
}

// multiple require sources
if (foo) {
  require('a');
} else {
  require('b');
}

// ...

我们做的一些事情包括。1)验所有 Bigfish 3 和 4 项目里用到的所有前端 npm 包及用到的所有版本(1800+),确保构建能过,且 require 入口文件不会报错,2)验所有 Bigfish 4 项目(2000+),确保构建能过,3)确保 Tree Shaking 不误删代码,自动 + 手动的方式确保抽样 100+ 的项目用 mako 构建后跑起来没问题,4)补充 200+ e2e 用例和 20+ 的 HMR 用例,这些用例是我们后续发版和重构的信心来源。过程中发现大量的问题,完成后,Mako 发布时就很有信心了。

构建性能是我们做 Mako 的根本原因,所以也花了很多精力在这块。需要注意的是,不是用 Rust 写的代码都快,只是 Rust 给了更多快的可能性,包括内存分配、并发、缓存等,得充分利用后才会变快。

要注意构建性能是多维的,比如下方这些。同时,这些性能维度在不同类型的项目之间也是有差异的,比如一个 chunk 的项目和 100 个 chunk 的项目之间,比如 1000 个模块的脚手架和 20000 个模块的巨型项目之间。

1)dev 的冷启动、热启动(带缓存)、HMR
2)build 的冷执行和热执行(带缓存)
3)HMR 还可细分是 leaf 更新还是 root 更新

以下是去年 1124 发布时统计的对比数据。

我们做的事情包括。

1)使用更高效的内存分配器(mimalloc-rust、tikv-jemallocator)
2)充分利用缓存和并发,基于 rayon 和 tokio
3)数据结构上引用优于 Clone
4)用 422 – 《Xcode Instruments》、benchmark(基于 hyperfine)206 – 《Benchmark》、puffin 等工具找性能卡点
5)桶文件优化方案 355 – 《optimizePackageImports》
6)…

举一个用 XCode Instruments 分析到的具体例子:现在的方案是先并行处理非 entry chunk 的,然后处理 entry chunk。经讨论,entry chunk 也可以和其他 chunk 一起做并行,在这个场景下,M2 Pro Max 下,构建阶段理论上可以提升 2s 左右。

再举一个具体例子:对某个正则实例化进行缓存处理后,性能提升 25% – 30%。

同时,我们在构建性能上还有很多计划和正在做的事情。(见《脑暴: 如何让 mako 的 dev 启动变的更快》

1)按需编译
2)持久缓存
3)MFSU
4)更多并发优化
5)Rust 版 Less
6)类 webpack 的 noParse
7)…

对于构建工具来说,光构建快是不够的,还得让打包出来的产物跑起来快。要想跑起来快,得做两件事,1)产物足够小,2)拆包策略足够好。前者很好理解,但是产物整体尺寸小不代表跑起来就快(访问某个页面时需要下载的 JS 和 CSS 少)。

拆包策略也是个复杂的事,得综合考虑产物整体大小(少重复)、单个页面加载的大小、更新时涉及的用户侧缓存更新等方面。虽然没有一个大一统的方案可以满足所有需求,但社区倒是有个复杂的最佳实践。

为了让产物足够小,我们也做了不少事。

1)压缩(dead code 移除、…)
2)Tree Shaking
3)常量折叠
4)Module Concatenation(或者叫 Scope hoisting
5)运行时优化
6)…

TODO:如果有时间,可以适当展开说下。

e.g. 常量折叠。

if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}

TODO:DX,有时间的话补充下。

落地。

1)2023.10.24,Mako 发布 RC
2)2023.11.24,Mako 在内网正式发布
3)2023.12,落地 Bigfish(中后台框架,新项目默认开启)、Minifish(小程序,Bundless 构建,325 – 《preserveModules》)、闪蝶部分场景(搭建)

截止 2024.03.26,其中已发布的 Bigfish 应用使用 Mako 的占 120+。同时今年,会接入更多场景,进行中的包括内网的云凤蝶(Low Code 平台)、Riddle(Code Snip 平台,使用 WASM 产物)、Smallfish(移动端 H5)、闪蝶更多场景、…,外网的 Dumi(antd 在用的组件库工具)、Father(组件打包工具)、… 。


从落地到开源

我们计划于 2024.06.28(预)开源,给社区另一个前端构建工具的选择。谈到开源,避不开的一个问题是,你的卖点是什么?和社区现有方案的区别是什么?目前的答案如下。

1)场景和 Edge Case 覆盖率(蚂蚁这么多项目帮你背书)
2)速度(力求业界第一)
3)性能(产物尺寸和 Webpack 靠齐,优化的拆包策略)
4)Umi 深度集成(元框架不仅仅是构建)
5)WASM 产物(可跑于浏览器侧)(待定)

举个例子,比如 Umi 深度集成。通过 node --cpu-prof + speedscope 分析,下方是 Umi 命令行在构建之前的性能时序图,某个大项目下需要 10s。预计通过和 Mako 深度集成,提升 Umi 的 Rust 含量,以上项目可以优化到 2-3s。详见 426 – 《Umi Cli 提速》

从落地到开源还需要做什么?

1)持续提升构建性能和产物性能
2)提升代码质量
3)邀请内测,有些公司在深度使用 Umi,比如浩鲸和快手等,会提前邀请内测以覆盖更多业务场景
4)Benchmark 数据(和社区方案的对比)

展开说下代码质量。开源通常两种策略,1)出生即开源,2)开源前猛补一波代码质量。Mako 属于后者,如果你有仔细观察,其实很多开源项目也是如此的。因为通常前期为了快速上功能赶里程碑,会做不少短时的战术性编程的事,引入了不必要的复杂度。代码质量就是关于复杂度的事情,这里不展开了。如果大家对提升代码质量和设计能力感兴趣,推荐一本书 413 – 《读书笔记:A Philosophy of Software Design》


Thanks