使用 Chrome DevTools 分析 Node.js 性能问题

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

在本节中则主要分享使用 Chrome DevTools 分析 Node.js 性能问题,以下为正文内容。

知识准备

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

《JavaScript 运行机制详解:再谈 Event Loop》:

www.ruanyifeng.com/blog/2014/1...

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

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

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

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

ini 复制代码
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 个宏任务之间的间隙,主线程可以继续执行其他同步代码。 优化后火焰图如下所示:

相关推荐
打野赵怀真22 分钟前
render函数中return如果没有使用()会有什么问题?
前端·javascript
Riesenzahn24 分钟前
写一个左中右的满屏布局,左右固定220px,中间自适应并且要优先加载
前端·javascript
Riesenzahn25 分钟前
css在页面上画一个正方形,边长为页面宽度的一半
前端·javascript
tommyrunner26 分钟前
Cursor rule文件测试 一秒了解AI行为规则文件
前端·cursor
北京_宏哥32 分钟前
《手把手教你》系列基础篇(九十三)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-上篇(详解教程)
java·前端·selenium
Nu1136 分钟前
weakMap 和 weakSet 原理
前端·面试
顾林海38 分钟前
深入理解 Dart 函数:从基础到高阶应用
android·前端·flutter
比特鹰42 分钟前
桌面端跨端框架调研
前端·javascript·前端框架
Ratten43 分钟前
【JavaScript】---- JS原生的深拷贝API structuredClone 使用详解与注意事项
前端·javascript