脚手架性能分析
以 rsBuild 为例
rsBuild 官网有构建 1000 个 React 组件的性能分析
介绍 - Rsbuild (opens new window)
以上数据基于 Farm 团队搭建的 benchmark,更多信息请参考 performance-compare (opens new window)。
我们可以参照 benchmark 来生成自己项目的性能报告
# 拷贝 benchmark.mjs
首先将 benchmark.mjs (opens new window) 拷贝到项目根目录
# 安装依赖
benchmark.mjs
需要 puppeteer
和 tree-kill
这两个包,需要下载一下
注意 puppeteer
需要安装 ^19.7.4
的版本,最新的版本是 20+,运行有问题
pnpm i tree-kill puppeteer@19.7.4 -D
# 统一端口号和运行命令
在 benchmark.mjs
中搜索 buildTools
,是一个数组
由于这个工具是测试很多脚手架的,而我们目前只需要测试 rsbuild
,所以需要将里面非 rsbuild
的部分注释掉
然后看一下 BuildTool
的参数:
new BuildTool(
'Rsbuild 0.2.0',
8080,
'start:rsbuild',
/in (.+) (s|ms)/,
'build:rsbuild',
/in (.+) (s|ms)/,
'@rsbuild/core/bin/rsbuild.js'
)
2
3
4
5
6
7
8
9
第一个参数是脚手架名称,第二个参数是运行端口号,第三个参数是运行命令,第四个参数是匹配运行命令启动后的正则,第五个参数是编译命令,第六个参数是匹配编译命令完成后的正则,第七个参数是脚手架的路径
然后修改 Rsbuild
第 2、3 和 5 参数,将端口号和运行命令,与 rsbuild.config.js
中的端口号保持一致,与 package.json
中的运行命令保持一致
new BuildTool(
'Rsbuild 0.2.0',
8081,
'start',
/in (.+) (s|ms)/,
'build',
/in (.+) (s|ms)/,
'@rsbuild/core/bin/rsbuild.js'
),
2
3
4
5
6
7
8
9
以下是 rsbuild.config.js
的示例
server: {
port: 8081,
},
2
3
以下是 package.json
的示例
"scripts": {
"start": "rsbuild dev",
"build": "rsbuild build",
},
2
3
4
注意 start 的时候不要使用 open 参数来打开浏览器
# 修改根文件和叶子文件
全局搜索 triangle.jsx
,将 resolve
中的路径修改为项目启动后打开页面对应的根文件
然后搜索 triangle_1_1_2_1_2_2_1.jsx
,将 resolve
中的路径修改为项目启动后打开页面中的叶子文件
为后续计算根文件和叶子文件热更新速度提供依据
# 启动
使用 node benchmark.mjs
来启动
等待片刻即可看到结果
(index) | startup(serverStartTime + onLoadTime) | rootHmr | leafHmr | buildTime |
---|---|---|---|---|
Rsbuild 0.2.0 | '13319ms' | '2591ms' | '1685ms' | '435ms' |
# 问题
# buildTime 异常
在上方表格中可以看到 buildTime
为 435ms,半秒钟就编译完成了,这应该是不可能的,检查一下 build 代码,看看哪里出问题了
async build() {
return new Promise(async (resolve) => {
console.log(`Running build command: ${this.buildScript}`);
let startTime = null;
const child = spawn(`npm`, ['run', this.buildScript], {
stdio: ['pipe'],
shell: true,
});
child.stdout.on('data', (data) => {
const startMatch = startConsoleRegex.exec(data.toString());
if (startMatch) {
startTime = startMatch[1];
}
const match = this.buildRegex.exec(data.toString());
if (match) {
if (!startTime) {
throw new Error('Start time not found');
}
resolve(Date.now() - startTime);
}
});
return new Promise((resolve, reject) => {
child.on('exit', resolve);
child.on('error', reject);
});
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
我们来解读一下上面的代码:
先是定义了一个 startTime
变量,用来存储编译开始的时间
然后定义了一个 child
变量,用来存储子进程的信息
然后我们用 child.stdout.on('data', (data) => {})
监听了子进程的输出(就是命令行的输出)
然后定义了一个 startMatch
变量,用来存储子进程输出中的 startConsoleRegex
的匹配结果,startConsoleRegex = /Farm Start Time (\d+)/
;
然后定义了一个 match
变量,用来存储子进程输出中的 this.buildRegex
的匹配结果,buildRegex = /in (.+) (s|ms)/
然后在匹配成功的时候我们就用 resolve(Date.now() - startTime)
来返回结果,这样就完成了 buildTime
的计算
可能就是正则匹配出错了,需要检查一下
我们输出一下 startMatch
和 match
🚀 ~ BuildTool ~ child.stdout.on ~ startMatch: [
'Farm Start Time 1708585068291',
'1708585068291',
index: 0,
input: 'Farm Start Time 1708585068291\n',
groups: undefined
]
🚀 ~ BuildTool ~ child.stdout.on ~ match: [
'in output.cssModules.localIdentName is currently not s',
'output.cssModules.localIdentName is currently not',
's',
index: 26,
input: "warn Custom hashDigest in output.cssModules.localIdentName is currently not supported when using Rspack, the 'base64' will be ignored.\n",
groups: undefined
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
startMatch
匹配的结果是正确的,match
匹配的结果应该是错误的
然后我们手动执行一下 build
命令,就可以看到子进程的输出结果了
> rsbuild build
Farm Start Time 1708583173367
Rsbuild v0.2.18
postcss-px2rem: postcss.plugin was deprecated. Migration guide:
https://evilmartians.com/chronicles/postcss-8-plugin-migration
warn Custom hashDigest in output.cssModules.localIdentName is currently not supported when using Rspack, the 'base64' will be ignored.
● Client ━━━━━━━━━━━━━━━━━━━━━━━━━ (100%) dones
ready Client compiled in 8.9 s
info Production file sizes:
2
3
4
5
6
7
8
9
10
11
对比上方的输出,buildRegex
应该匹配 in 8.9 s
才是对的
但由于上方 cssModules 的警告输出,导致正则匹配错误,还没有执行构建,就认为构建完成了
所以我们修改一下 buildRegex
,让其匹配 in 8.9 s
,就可以了
/Client compiled in (.+) (s|ms)/
这样再执行就没有问题了
(index) | startup(serverStartTime + onLoadTime) | rootHmr | leafHmr | buildTime |
---|---|---|---|---|
Rsbuild 0.2.0 | '9103ms' | '1972ms' | '1017ms' | '9366ms' |
顺便把 startedRegex 也改一下吧
# 原理分析
该脚本共统计了 4 个指标,分别是 项目启动时间(脚手架启动和页面 loaded,startup)、根文件热更新时间(rootHmr)、叶子文件热更新时间(leafHmr)、构建耗时(buildTime)
# 项目启动时间
serverStartTime
为脚手架启动的时间,onLoadTime
为页面加载完成的时间
先看 serverStartTime 的计算逻辑:
先向脚手架入口文件注入一段输出当前时间的代码
"console.log('Farm Start Time', Date.now());";
然后运行 start 命令
监听子进程的输出
在输出中匹配
Farm Start Time
,获取其时间戳,即为脚手架开始启动时间然后再匹配脚手架 Client compiled 时间,即为脚手架编译完成并打开页面时间
两者相减计算出脚手架编译启动时间
再看 onLoadTime 的计算逻辑:
这个就比较简单了:脚手架编译启动后,记录当前时间,然后使用 puppeteer 启动一个 browser,然后创建一个 page,监听 load 即可,loadTime - 记录的当前时间即为页面加载完成的时间
# 根文件热更新时间
先在根文件最后注入一段输出当前时间的代码,并记录当前时间
console.log('root hmr', Date.now());
page 监听 console 的输出
在输出中匹配
root hmr
,获取其时间戳,即为根文件热更新完成时间使用热更新完时间减去记录的当前时间即为根文件热更新时间
# 叶子文件热更新时间
与根文件热更新时间类似
# 构建耗时
与项目启动时间类似
# 统计时间
该脚本共计运行 3 次,取平均数作为最后的统计值
# vite 及其他脚手架性能分析
参考上方
- 01
- 搭配 Jenkins 实现自动化打包微前端多个项目09-15
- 02
- 自动化打包微前端多个项目09-15
- 03
- el-upload 直传阿里 oss 并且显示自带进度条和视频回显封面图06-05