Farm 是一个基于 Rust 语言编写的构建引擎,如果还不了解 Farm 可以参考 :比 Vite 快 5 倍? 2ms HMR?Farm:基于 Rust 的极速构建引擎!
从 Farm 第一次发布 v0.3 版本(2023 年 3 月 6 日),已经过去半年多的时间,期间 Farm 一直在持续开发,并且得到了很多社区开发者的贡献和支持,目前 Farm 已经实现了构建引擎需要的所有能力(包括生产环境优化如 tree shake,压缩,拆包,语法降级和 polyfill 等),基本达到生产环境可用的状态。
如果希望体验或者接入 Farm,可以参考官方文档:Farm Documentation | Farm
本次 v0.13 版本 Farm 更新了两个重磅功能:
- Vite/Rollup 插件兼容:Vite 插件可以直接在 Farm 中使用,目前 @vitejs/plugin-vue,vite-plugin-solid 等等插件都可以直接在 Farm 中使用!并且在完全使用 Vite 插件的情况下,Farm 依然比 Vite 快数倍。
- 新版局部打包算法: 实现了一套全新的打包算法,可以自动根据依赖关系、大小等因素,将每次资源加载需要的模块打包成 25 个左右的文件,实现确保最优加载性能的同时,进一步提升缓存命中率。
对于新版打包算法的技术细节,可以参考 RFC-003 Partial Bundling
一、Vite/Rollup 插件兼容
从 V0.13 版本开始,Farm 计划在 JS 侧直接复用 Vite/Rollup 的现有生态,对于现有功能 JS 插件直接使用 Vite 即可,Farm 后续将专注 Rust 化,即使用 Rust 语言重写常用工具或者开发新工具,而不在 JS 生态上再重复开发。
1.1 兼容 Vite 插件后,Farm 还有何优势?
一些开发者可能会提出疑问,既然已经兼容 Vite 生态,为何不直接使用 Vite?用 Farm 有什么优势呢?这个问题涉及到 Vite 和 Farm 基本理念,在 比 Vite 快 5 倍? 2ms HMR?Farm:基于 Rust 的极速构建引擎! 中有提及。
简单来说,笔者认为,Vite 目前并不完美,存在以下几个问题:
- 开发环境的巨量请求:对于一个较大型的项目,开发环境的首屏请求数量可能达到数千,冷启动刷新一次页面需要数十秒,热启动后刷新也需要 10s,极大影响开发体验
- 开发环境和生产的不一致:Vite 的开发环境和生产环境存在很大的不一致,从机制到底层使用的工具链都完全不同,比较割裂,一旦出现开发生产不一致问题,排查起来非常痛苦
- 拆包算法不够灵活:Vite 目前复用的 rollup 的拆包算法,而 rollup 的拆包算法配置起来非常不灵活,比如希望限制产物最小大小、最大大小、控制并发请求数量等实现起来非常困难。
而 Farm 可以完美解决上述问题,Farm 在开发生产采用完全一致的策略,同时有一套非常灵活且细粒度的打包算法。并且,在 Farm 中使用 Vite 插件后,依然比 Vite 数倍!
我们迁移了一个后台 Vite 模板项目(github.com/farm-fe/far...)到 Farm,在几乎完全复用 Vite 插件的基础上,Farm 仍然比 Vite 在开发环境快 2 倍,生产环境快 6 倍!性能对比如下:
Dev 冷启动(Server 启动 + 页面渲染) | 生产构建 | |
---|---|---|
Farm | 3.5s | 4s |
Vite | 7.5s | 24.1s |
对比 | Farm 快 2 倍 | Farm is 快 6 倍 |
1.2 为什么选择兼容 Vite 生态?
Farm 选择兼容 Vite 生态有如下几点原因:
- 上面已经提到,我们计划减少 Farm 在 JS 生态上的持续投入,尽可能复用现有 JS 生态,从而提升后续 Farm 在 Rust 化上面的投入
- Vite 生态当前最活跃,新技术、框架都支持 Vite,兼容 Vite 也有助于 Farm 生态的长远发展
- Farm 采用了一套和 Vite/Rollup 类似的插件系统,兼容起来成本比较低
1.3 如何在 Farm 中使用 Vite 插件
首先安装 Vite 插件到项目中,以 plugin vue 为例:
bash
pnpm add -D @vitejs/plugin-vue vite
然后在 farm.config.ts 中配置即可:
ts
import type { UserConfig } from '@farmfe/core';
import vue from '@vitejs/plugin-vue';
function defineConfig(config: UserConfig) {
return config;
}
export default defineConfig({
vitePlugins: [vue()]
});
添加插件后,Vue SFC 就可以直接在 Farm 项目中使用了!
目前大部分常用的 vite transformer 插件已经能直接在 Farm 中使用了。不过目前版本(v0.13) ,还只支持 build stage 的钩子,后续 Farm 会兼容 generate stage 的钩子。 如果遇到不兼容的 Vite 插件,Farm 会报错提示。
二、局部打包算法
局部打包即 Partial Bundling 。局部打包的技术细节暂不在此文讨论,后续单独分析,感兴趣可以参考:RFC-003 Partial Bundling
2.1 什么是局部打包?
Partial Bundling 是 Farm 用于打包模块的策略,与其他 bundler 的做法类似,但 Farm 的 Partial Bundling 的理念有所不同。
与其他 bundler 不同,Farm 不会尝试将所有内容打包在一起,然后使用 splitChunks
等优化将它们分开,相反,Farm 会将项目直接打包到多个输出文件中。 例如,如果启动一个 html 页面需要数百个模块,Farm 将尝试直接将它们打包到 20-30 个输出文件中。 Farm 将这种行为称为 Partial Bundling
。
Farm Partial Bundling
的目标是:
- 减少请求数量和请求层次:使数百或数千个模块请求减少到20-30个请求,并避免由于依赖层次而逐个加载模块,这将使资源加载更快。
- 提高缓存命中率:当模块更改时,确保只有少数输出文件受到影响,对于线上项目尽可能复用更多缓存。
对于传统的 bundler,我们可能很难配置复杂的splitChunks
或manualChunks
来同时实现上述两个目标,但在 Farm 中,通过Partial Bundling
原生支持。
2.2 局部打包规则
在本节中,我们将通过示例介绍Partial Bundling使用的基本规则。
首先我们研究一个基本的 React 项目示例。 对于像下面这样的基本 react 项目,我们在入口脚本中导入 react 和 react-dom:
tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import './index.scss';
const container = document.querySelector('#root');
const root = createRoot(container);
root.render(
<>
<div>Index page</div>
</>
);
打包结果将如下所示:
bash
./dist/
├── index_9c07.49b83356.js # 包含react-dom
├── index_a35f.0ac21082.js # 包含./index.tsx
├── index_b7e0.7ab9ca2d.js # 包含react及其依赖项
├── index_ce26.7f833381.css # 包含./index.scss
└── index.html # 包含./index.html
默认情况下,Farm 会将项目打包为 5 个产物文件:
- 2 个 js 文件来自 node_modules,包含 react、react-dom 及其依赖项。
- 1 个js文件来自./index.tsx
- 1 个 css 文件来自./index.scss;
- 1个html文件来自./index.html;
以上结果来源于 Farm 使用的下述规则:
- 可变和不可变模块应始终位于不同的输出文件中:默认情况下,Farm 认为 node_modules 下的所有模块都是不可变的,否则是可变的。 所以 ./index.tsx 位于一个单独的文件中,因为它是一个可变模块,所以它永远不会与 react 和 react-dom 位于同一个输出文件中。
- 不同类型的模块始终位于不同的输出文件中:因此 ./index.scss 位于单独的文件中。
- 同一包中的模块应该位于同一个输出文件中:因此所有react模块始终位于同一个输出文件中,react-dom也是如此。
- 默认情况下,资源加载的目标并发请求数应在 20-30 之间:因此有 3 个 js 输出文件,而不是 1 个 js 包。
- 输出文件大小应相似,默认最小资源大小应大于20KB:因为react-dom最大,超过100KB,所以它在一个单独的文件中,而 react 的依赖项小于20KB,被合并到同一个输出文件中。
因此通过配置 可变不可变
,请求数量
,资源大小
等因素,就可以非常灵活的控制 Farm 的默认打包行为。同时,也可以通过 gourps
等配置来指定打包行为。
2.3 局部打包配置
局部打包支持大量的配置项,详情请参考文档:Partial Bundling | Farm,本文仅仅简要讨论常用配置的用法。
通过 groups 配置可以实现将指定模块打包到一起,示例配置如下:
ts
export default defineConfig({
compilation: {
partialBundling: {
groups: [
{
name: 'vendor-react',
test: ['node_modules/react', 'node_modules/react-dom'],
}
]
},
},
});
上述配置将 react 和 react-dom 分组到一起,在输出产物时,他们会尽可能在同一个产物文件中。不过请注意,groups并不强制打包所有与该组匹配的模块,一个group会生成多个产物文件,因为以下规则的限制:
- 可变和不可变模块始终位于不同的输出文件中。 当可变模块和不可变模块都命中这个组时,它们将处于不同的输出文件中。
- 当涉及多页面应用程序或 dynamic import 时,可能存在共享模块,这些入口不同的共享模块会始终位于不同的输出文件中。
如果需要强制打包指定的模块到一个文件中,可以使用 enforceResources
配置,详情可以参考文档。
三、总结
Farm 经过近一年紧锣密鼓的开发,以及众多社区开发者的贡献和努力,已经距离 1.0 版本越来越近了,当前也已经基本达到生产可用的状态,并且有真实的线上项目在使用 Farm!我们计划在 2023 年底前发布 1.0 版本,目前距离 1.0 版本,还缺少如下两个特性:
- 持久缓存:缓存构建产物到磁盘,并在多次构建中复用,进一步提速项目冷启动
- 90% 的 Vite 插件兼容:兼容 generate stage 钩子,90% 的 Vite 插件都能无缝接入 Farm
在此也再次感谢众多社区贡献者对 Farm 的贡献和投入,也欢迎更多贡献者加入 Farm 兴趣团队。我们的目标是做更好的工具,提供更加优质的开发体验!