前端项目构建时的资源监控与分析
在 CI 环境打包前端项目时,你或许遇到过这样的错误(OOM):
<--- Last few GCs --->
[1:0x63b6120] 122046 ms: Mark-sweep (reduce) 2003.3 (2005.1) -> 2003.2 (2005.1) MB, 4.1 / 0.5 ms (+ 0.1 ms in 1 steps since start of marking, biggest step 0.1 ms, walltime since start of marking 47 ms) (average mu = 0.999, current mu = 0.999) external
<--- JS stacktrace --->
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
又或这样:构建进程直接退出,连一点多余的错误信息都没有留下。
Killed
error Command failed with exit code 137.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
遇到类似的问题,尝试设置 max-old-space-size
参数是一条可行的解决之道。但是你有没有思考过为什么这个配置会起作用呢?另外,构建工具相关文档中涉及到资源消耗的内容并不多。因此本文希望结合几个实验,借助一些工具来分析打包过程中的资源消耗问题,解答这些疑问。
基础知识
在 Node.js 中,提供多种形式的方法来测量统计性能方面的数据。对于前端项目构建过程而言,我们主要关注「内存」与「CPU」指标。
内存监控
获取 Node.js 内存相关的数据是非常简单的,主要使用 process
与 os
这两个包。
import { freemem, totalmem } from 'os';
const { rss, heapUsed, heapTotal } = process.memoryUsage();
const sysFree = freemem(); // 获取系统空闲内存
const sysTotal = totalmem(); // 获取系统总内存
借助以上的数据可以计算出内存占用率。
heapUsed / heapTotal; // 堆内存占用率
rss / sysTotal; // 进程内存占用率
1 - sysFree / sysTotal; // 系统内存占用率
这里我们重点关注堆内存。堆内存是指 V8 引擎所使用的内存,它主要用于存储 JavaScript 对象、变量和函数等数据。在 Node.js 应用程序中,大部分内存消耗都来自于堆内存,设置 max-old-space-size
来调整堆内存的大小。
Profile
为了监控 Node.js 应用程序中的内存使用情况,我们还可以使用内存监控工具,如 V8 profiler。这些工具可以帮助我们识别内存泄漏并读取和分析内存快照。可以通过如下的方式来获取内存快照。最后将生成的 heapsnapshot 文件导入 Chrome devtool 即可分析内存快照。
import { Session } from 'inspector';
const session = new Session();
session.connect();
async function dumpProfile() {
session.on('HeapProfiler.addHeapSnapshotChunk', (m) => {
writeFileSync('profile.heapsnapshot', m.params.chunk);
});
await session.post('HeapProfiler.takeHeapSnapshot', null);
}
由于本文不涉及到内存数据的分析,对于此工具感兴趣的读者可以查阅相关文档。
CPU 分析
process.cpuUsage
用于获取进程 CPU 时间的方法,它返回一个包含用户 CPU 时间和系统 CPU 时间的对象。用户 CPU 时间表示进程使用 CPU 的时间,而系统 CPU 时间表示操作系统使用 CPU 的时间。process.hrtime.bigint
方法是一个高精度计时器,用于获取当前时间的纳秒级别的精确时间戳,返回一个 BigInt 类型的值。结合这两者可以计算出 CPU 利用率。
const startTime = process.hrtime.bigint();
const startUsage = process.cpuUsage();
doSomething();
const endTime = process.hrtime.bigint();
const endUsage = process.cpuUsage(startUsage);
const duration = Number(endTime - startTime) / 1000; // ms
(endUsage.user + endUsage.system) / duration; // cpu 利用率
max-old-space-size 的作用
现在我们已经掌握了足够的基础知识,回到文章开头提到的 OOM 问题,来看一下设置 max-old-space-size
对 Node.js 进程的影响。
通过以下的方式可以计算出最大堆内存大小。
import { getHeapStatistics } from 'v8';
Math.floor(getHeapStatistics().heap_size_limit / 1024 / 1024);
在一个 4GB
的 Node.js v16 执行上述脚本得到的最大对内存值为 2015M
。编写一个简单的脚本来测试内存。
// 改编自 https://github.com/mcollina/climem/blob/master/app.js
const array = [];
setInterval(() => {
array.push(Buffer.alloc(1024 * 1024 * 50).toString()); // 50M
}, 3000);
最终内存数据的变化指标如下图所示。Node.js 进程在 122 秒后出现 OOM 问题,此时堆内存非常接近 heap_size_limit
,另外还有空闲内存 700M
。
参考Node.js 官方文档提供的建议,设置 max-old-space-size=3584
后再次执行脚本。内存变化指标如下所示。此时该进程在 220 秒后才出现 OOM 问题,剩余的空闲内存快接近于 0。
从以上的变化曲线可以看出,将 max-old-space-size
调大确实可以充分地利用内存,从而做到减少构建过程中 OOM 问题。
构建分析
使用 create-react-app
新建个项目,让我们结合具体的前端项目来进行分析。
Webpack ProfilingPlugin
ProfilingPlugin 是一个非常好用的插件,可以很方便地生成 Profile 文件用以分析构建过程。通过以下方式开启该插件。
{
plugins: [
// ...
new ProfilingPlugin({
outputPath: join(__dirname, 'profile', `profile.json`),
}),
];
}
将生成的 profile.json
文件导入到 Chrome devtools 中的 performance
面板得到如下的结果。
耗时较长的几个 Plugin 如下表所示:
ProfilingPlugin 生成的 profile 不包含内存统计信息,因此还需要编写一个简单的内存统计插件。结合内存监控数据的变化可以探索更多的细节。
不要在业务项目中使用 ProfilingPlugin,该插件消耗资源多,另外生成 profile 文件非常大,直接导入 Chrome devtools 甚至会崩溃。
内存监控
基于前文介绍的基础知识,编写一个内存监控插件是非常容易的。向 compiler
示例中注册相应的生命周期,用以开始监控或者上报监控数据。
// 按照一定的间隔收集数据
async function collectMemoryUsage() {}
class MemWatchPlugin {
apply(compiler) {
// 构建开始前
compiler.hooks.beforeRun.tap(pluginName, collectMemoryUsage);
// 构建结束后
compiler.hooks.done.tap(pluginName, saveMemoryUsageData);
// 构建失败后
compiler.hooks.failed.tap(pluginName, saveMemoryUsageData);
}
}
对收集到的数据进行可视化处理,得到如下的内存变化趋势图。
分析
忽略一些耗时极短的 Plugin 后,将前两步得到的数据结合在一起,大致得到下面这样一张图。
将图中的这几个阶段与 Plugin 对应起来如下表所示。注意到此时图中 x 轴(时间)在 1,2 阶段的增长比较迅速。内存监控的函数是在一个定时器中触发的。也可以从侧面说明这两个阶段消耗了大量的资源,影响到了定时器的触发。在 js 代码压缩完成后,内存会有一个小幅度的下降。在完成 js/css 压缩后,后面都没有什么特别耗时的任务了,构建也基本要结束了。
max-old-space-size 不生效的场景
如果在项目中中引入了 monaco-editor ,构建时内存的变化趋势将会更加明显。下图是一个真实的业务项目内存变化趋势图。并且随着构建的进行,可用的内存越来越少,时刻在内存耗尽的边缘徘徊。针对这种情况,设置 max-old-space-size
的作用不大了,最有效的解决方案是将 monaco-editor
这类的包配置成 externals
。
总结
本文介绍了测量统计性能相关的工具,并且结合具体的前端项目构建案例,分析了构建过程中的资源消耗,从而更好地帮助前端开发者更加深入地理解构建过程,做到知其然更知其所以然。读者在阅读完本文后,下次遇到构建性能优化问题时,也有一定的解决方案策略。
转载自:https://juejin.cn/post/7220962426397311013