Node.js 技术原理分析系列——使用 Chrome DevTools 分析 Node.js 性能问题

举报
OpenTiny 发表于 2025/03/06 15:15:25 2025/03/06
148 0 0
【摘要】 Node.js 是一个开源的、跨平台的 JavaScript 运行时环境,它允许开发者在服务器端运行 JavaScript 代码。Node.js 是基于 Chrome V8 引擎构建的,专为高性能、高并发的网络应用而设计,广泛应用于构建服务器端应用程序、网络应用、命令行工具等。本系列将分为9篇文章为大家介绍 Node.js 技术原理:从调试能力分析到内置模块新增,从性能分析工具 perf_h...

Node.js 是一个开源的、跨平台的 JavaScript 运行时环境,它允许开发者在服务器端运行 JavaScript 代码。Node.js 是基于 Chrome V8 引擎构建的,专为高性能、高并发的网络应用而设计,广泛应用于构建服务器端应用程序、网络应用、命令行工具等。

本系列将分为9篇文章为大家介绍 Node.js 技术原理:从调试能力分析内置模块新增,从性能分析工具 perf_hooks 的用法到 Chrome DevTools 的性能问题剖析,再到 ABI 稳定的理解、基于 V8 封装 JavaScript 运行时、模块加载方式探究、内置模块外置以及 Node.js addon 的全面解读等主题,每一篇都干货满满。

在上一节中我们探讨了 Node.js 的 perf_hooks 模块作用和用法,在本节中则主要分享使用 Chrome DevTools 分析 Node.js 性能问题,本文内容为本系列第4篇,由体验技术团队屈金雄原创,以下为正文内容。

知识准备

无论是学会使用 Node.js 性能分析工具,还是解决性能问题,都要理解运行原理,其中有几个点尤为重要,这里仅提及一下,包括单线程、事件循环、异步、宏任务、微任务等。阮一峰的 《JavaScript 运行机制详解:再谈 Event Loop》,这篇文章讲的很好,但是不够全面,有些点甚至过时了。如果需要更准确深入的了解 Node.js 运行原理,恐怕只能去啃源码了。

《JavaScript 运行机制详解:再谈 Event Loop》:https://www.ruanyifeng.com/blog/2014/10/event-loop.html

Performance 面板介绍

Chrome DevTools 中的 Performance 面板和 Chrome 开发者工具中的 Performance 面板不是同一个,前者用于 Node.js 性能分析,后者用于 web 前端性能分析。 如下图所示,Chrome DevTools 的 Performance 面板包括以下四个区域:

  1. 操作区
  2. 时间轴:包括从记录开始到记录结束这个时间段。
  3. 概览区:目前只能看主线程活动,以后也许能看更多内容。
  4. 详情区:详情区的内容会随鼠标左键选中的目标变化而变化。

火焰图

火焰图在很多性能分析工具中都有实现,Chrome DevTools 中的实现就是前面提到的主线程活动(点击 Main 旁边的三角可以收缩/展开这个区域)。 读懂火焰图需要理解以下几个关键点:

  1. 火焰图是由很多色块堆叠(内层函数堆叠在外层函数下面)而成,每一个色块对应一个函数。
  2. 函数耗时用宽度表示。我们可以通过肉眼观察色块宽度,来发现哪个函数耗时最长。
  3. 最上面一层都是一个宏任务对应的任务(这个很关键),宏任务之间是异步的,不会阻塞。

举例说明

一、构造有性能问题的代码

如下代码所示,函数 bigTask1 是构造的有性能问题的代码。这个函数将一个比较大的文件读取10次。我们模拟一下平时常见的业务场景,用 node 编写一个迷你的 web 后端服务,将这个函数放在一个请求的回调函数中,通过发起请求来触发执行。

const { createServer } = require('node:http');
const { readFileSync } = require('node:fs');
const { performance } = require('node:perf_hooks');

const hostname = '127.0.0.1';
const port = 3000;

const server = createServer((req, res) => {
  bigTask1();
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World');
});

server.listen(port, hostname, async () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

/**
 * 假设我们需要读取10次文件
 * 同步连续读取10次,那么主线程在读取完成前都不能处理其他任务。
 */
functionbigTask1() {
  let count = 0;

functionchunk() {
    for (let i = 0; i < 10; i++) {
      // 每次处理一小部分
      console.log(count);
      readBigFile();
      count++;
    }
  }

  chunk();
}
/**
 * 构造的长耗时任务
 */
functionreadBigFile() {
  const start = performance.now();
  readFileSync('D:\qujinxiong\资料\node-18.20.2.zip');
  const end = performance.now();
  console.log(`操作耗时: ${end - start} 毫秒`);
}

二、启动服务并连接

具体操作在Node.js 调试能力分析 中有介绍,这里不再赘述。 连接成功后,切换到 Chrome DevTools 的 Performance 面板,结果如下图所示:

三、点击 DevTools 左上角录制按钮(Record 按钮),开始录制

四、向服务器发起一次请求

打开新的标签页,向服务器发起一次请求

五、点击 DevTools 左上角录制按钮(Record 按钮),停止录制

步骤三、四、五的操作是为了记录发起请求后的性能数据,如下图所示:

六、分析性能数据,定位问题

采集到正确的性能数据后,可以按照以下三步来逐步定位问题:

  • 第一步:Bottom-Up 分析
    由于数据量太大,我们需要工具帮助我们初步锁定问题。这个步骤一般来说是性能数据分析的第一步。 如下图所示,这个面板下方,用列表展示各个函数自身耗时,通过这个列表可以一眼看出耗时最长的函数,这里是 parserOnIncoming 函数,耗时 608.4ms。 一般来说单函数耗时超 50ms 就值得关注了,性能问题可能就出在这种函数中。 Tips:图中 read 函数共执行了 10 次,列表中的 546.0ms 是 10 次之和。

  • 第二步:火焰图分析
    鼠标悬停在火焰图上,可以放大火焰图。 上一步找到大函数后,我们可以在放大的火焰图上找到该函数,对大函数进一步分析。 放大后,可以看到 10 次大文件读取都在一个宏任务中同步读取,同步读取 10 次大文件期间,主线程是阻塞的,读取需要多长时间,就阻塞多长时间,这里是 587ms。 至此,我们定位到了开头构造的问题。

  • 第三步:Call Tree 分析
    这个面板,算是对火焰图的列表形式的展示。 如下图所示,我们通过火焰图找到大函数 parserOnIncoming 的最上层函数 paserOnHeadersComplete 后,一步步展开 paserOnHeadersComplete,可以找到 bigTask1,这是我们自己写的代码。至此,我们定位到了问题代码的位置,从右侧给出的文件链接,我们还能直接打开文件,详细分析代码。

    在打开的文件中我们甚至可以加断点,再次触发该代码的执行,即可开始调试。

七、通过拆分宏任务实现优化

const { createServer } = require('node:http');
const { readFileSync } = require('node:fs');
const { performance } = require('node:perf_hooks');

const hostname = '127.0.0.1';
const port = 3000;

const server = createServer((req, res) => {
  bigTask2();
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World');
});

server.listen(port, hostname, async () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});
/**
 * 将10次读取任务拆分的5个宏任务中
 * 这样的话,在5个宏任务的间隙,主线程可以继续处理其他任务,而不会阻塞
 */
functionbigTask2() {
  let count = 0;

  functionchunk() {
    for (let i = 0; i < 2; i++) {
      // 每次处理一小部分
      console.log(count);
      readBigFile();
      count++;
    }
    if (count < 10) {
      console.log('next macro!');
      setTimeout(chunk, 0); // 递归调用,延迟0毫秒继续处理下一部分
    }
  }

  chunk();
}

/**
 * 构造的长耗时任务
 */
functionreadBigFile() {
  const start = performance.now();
  readFileSync('D:\qujinxiong\资料\node-18.20.2.zip');
  const end = performance.now();
  console.log(`操作耗时: ${end - start} 毫秒`);
}

如上代码所示,同样是读取10次大文件,在 bigTask2 中,我们通过 setTimeout(这是一个宏任务),将 chunk 中的 10 次大文件读取,拆分到 5 个宏任务中,每个宏任务中读取两次。 这样由于宏任务是异步执行,5 个宏任务之间的间隙,主线程可以继续执行其他同步代码。 优化后火焰图如下所示:


下一节,将分享《理解 Node.js 中的 ABI 稳定》相关内容,请大家持续关注本系列内容~学习完本系列,你将获得:

  • 提升调试与性能优化能力
  • 深入理解模块化与扩展机制
  • 探索底层技术与定制化能力

同时欢迎大家给OpenTiny提建议:【OpenTiny调研征集】共创技术未来,分享您的声音!

关于OpenTiny

欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
OpenTiny 官网:https://opentiny.design
OpenTiny 代码仓库:https://github.com/opentiny
TinyVue 源码:https://github.com/opentiny/tiny-vue
TinyEngine 源码: https://github.com/opentiny/tiny-engine
欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI~ 如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

作者其他文章

评论(0

抱歉,系统识别当前为高风险访问,暂不支持该操作

    全部回复

    上滑加载中

    设置昵称

    在此一键设置昵称,即可参与社区互动!

    *长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

    *长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。