前端项目构建时的资源监控与分析

在 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 面板得到如下的结果。

图片

profile.json

耗时较长的几个 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。

图片

内存耗尽的例子

总结

本文介绍了测量统计性能相关的工具,并且结合具体的前端项目构建案例,分析了构建过程中的资源消耗,从而更好地帮助前端开发者更加深入地理解构建过程,做到知其然更知其所以然。读者在阅读完本文后,下次遇到构建性能优化问题时,也有一定的解决方案策略。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY4nWBPI' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片