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

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 后端服务,将这个函数放在一个请求的回调函数中,通过发起请求来触发执行。

js 复制代码
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,这是我们自己写的代码。至此,我们定位到了问题代码的位置,从右侧给出的文件链接,我们还能直接打开文件,详细分析代码。

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

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

js 复制代码
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标签,一起参与开源贡献~

相关推荐
一只大侠的侠6 小时前
Flutter开源鸿蒙跨平台训练营 Day 10特惠推荐数据的获取与渲染
flutter·开源·harmonyos
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅10 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
猫头虎10 小时前
如何排查并解决项目启动时报错Error encountered while processing: java.io.IOException: closed 的问题
java·开发语言·jvm·spring boot·python·开源·maven
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端