首页 / Node.js 教程 / 性能分析 Profiling

Node.js 教程

性能分析 Profiling

本教程共 76 篇 · 第 67 篇 · 更新于 2026-07-25 · 约 6 分钟阅读

Node.js性能分析profiling火焰图CPU

67. 性能分析 Profiling

本节目标:用 —prof 和火焰图分析 CPU 性能瓶颈。

“程序慢” 是一个症状,不是病因。本章教你用 Node.js 内置的 profiler 和其他工具,像医生做 CT 一样找到代码里的性能病灶。

内置 V8 Profiler:--prof

Node.js 内置了基于 V8 的 CPU profiler,不需要安装任何第三方包。原理是定期采样(默认每秒 1000 次),记录当前执行栈在哪些函数里停留,生成一个 tick 文件。

准备一个有性能问题的程序

我们写一个故意有问题的 HTTP 服务,用同步的 pbkdf2Sync 做密码哈希:

import http from 'node:http';
import crypto from 'node:crypto';

const users = new Map();

const server = http.createServer((req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);

  if (url.pathname === '/register') {
    const username = url.searchParams.get('username') || 'user';
    const password = url.searchParams.get('password') || 'pass';

    // 同步哈希,阻塞事件循环!
    const salt = crypto.randomBytes(128).toString('base64');
    const hash = crypto.pbkdf2Sync(password, salt, 100000, 512, 'sha512');
    users.set(username, { salt, hash });

    res.writeHead(200);
    res.end('registered\n');
    return;
  }

  if (url.pathname === '/auth') {
    const username = url.searchParams.get('username') || 'user';
    const password = url.searchParams.get('password') || 'pass';
    const user = users.get(username);

    if (!user) {
      res.writeHead(401);
      res.end('not found\n');
      return;
    }

    // 又是同步哈希!
    const hash = crypto.pbkdf2Sync(password, user.salt, 100000, 512, 'sha512');
    if (crypto.timingSafeEqual(hash, user.hash)) {
      res.writeHead(200);
      res.end('ok\n');
    } else {
      res.writeHead(401);
      res.end('fail\n');
    }
    return;
  }

  res.writeHead(404);
  res.end('not found\n');
});

server.listen(3000, () => console.log('监听 3000'));

保存为 slow-app.mjs

运行 profiling

# 1. 用 --prof 启动应用
node --prof slow-app.mjs

# 2. 在另一个终端施加负载(用 curl 或 ab)
curl "http://localhost:3000/register?username=test&password=123"
ab -n 50 -c 5 "http://localhost:3000/auth?username=test&password=123"

# 3. 停止应用(Ctrl+C),当前目录会生成一个 tick 文件
# 文件名类似:isolate-0x1234567890ab-v8.log

处理 tick 文件

# 用 --prof-process 处理日志
node --prof-process isolate-0x*-v8.log > profile.txt

打开 profile.txt,你会看到几个关键部分。

Summary(摘要)

 [Summary]:
   ticks  total  nonlib   name
     15    0.1%    0.1%  JavaScript
  15234   97.5%   99.3%  C++
     12    0.1%    0.1%  GC
    267    1.7%          Shared libraries
    102    0.7%          Unaccounted

97.5% 的时间花在 C++ 代码里。这说明瓶颈不在 JavaScript 层面,而在某个原生调用上。

C++ 部分

 [C++]:
   ticks  total  nonlib   name
   8234   52.7%   53.7%  node::crypto::PBKDF2(...)
   2101   13.4%   13.7%  _sha1_block_data_order
   1567   10.0%   10.2%  _malloc_zone_malloc

一目了然:52.7% 的 CPU 时间耗在 PBKDF2 上,也就是我们的 pbkdf2Sync

Bottom up (heavy) profile

   ticks parent  name
   8234   52.7%  node::crypto::PBKDF2(...)
   8234  100.0%    v8::internal::Builtins::~Builtins()
   8234  100.0%      LazyCompile: ~pbkdf2 crypto.js:557:16
   8234  100.0%        LazyCompile: *exports.pbkdf2Sync crypto.js:552:30

这里清晰地展示了调用链:pbkdf2Syncpbkdf2PBKDF2(C++ 层)。

修复并验证

把同步版本改成异步版本:

// 替换 pbkdf2Sync 为 pbkdf2
crypto.pbkdf2(password, salt, 100000, 512, 'sha512', (err, hash) => {
  if (err) { /* ... */ }
  users.set(username, { salt, hash });
  res.writeHead(200);
  res.end('registered\n');
});

重新用 --prof 跑一遍,PBKDF2 会从 C++ 采样里消失,事件循环不再被阻塞,并发能力大幅提升。

Note

--prof 的采样开销很小,可以在生产环境短时启用。但不建议长期开着,毕竟还是有额外负担。

Chrome DevTools:可视化的 profiler

命令行看数字不够直观,Chrome DevTools 提供了火焰图(Flame Graph)视图。

生成 CPU profile 文件

用内置的 inspector 协议:

node --inspect server.mjs

或者启动时立即断点:

node --inspect-brk server.mjs

打开 Chrome,访问 chrome://inspect,点击 “Open dedicated DevTools for Node”。在 Performance 标签页点击录制按钮,施加负载后停止,就能看到火焰图。

用编程方式导出 .cpuprofile

如果你想把 profiling 集成到 CI 流程里,可以用 inspector 模块程序化控制:

import inspector from 'node:inspector';
import fs from 'node:fs';

const session = new inspector.Session();
session.connect();

// 开始 CPU profiling
session.post('Profiler.enable', () => {
  session.post('Profiler.start', () => {
    console.log('CPU profiling 开始,10 秒后结束');

    setTimeout(() => {
      session.post('Profiler.stop', (err, { profile }) => {
        if (err) throw err;
        fs.writeFileSync('profile.cpuprofile', JSON.stringify(profile));
        console.log('已保存 profile.cpuprofile');
        session.disconnect();
        process.exit(0);
      });
    }, 10000);
  });
});

把这段代码和业务逻辑跑在一起,结束后得到 profile.cpuprofile,拖到 Chrome DevTools 的 Performance 面板就能分析。

火焰图怎么看

火焰图(Flame Graph)是把采样结果可视化的一种方式。每一层是一个函数,宽度代表该函数在采样中出现的比例。看火焰图的技巧:

  1. 找最宽的塔:宽就是耗时长,优化它收益最大。
  2. 从下往上看:底部是调用栈的根,往上是被调用的函数。
  3. 关注 “平顶”:如果某个函数的宽度几乎和它的父函数一样宽,说明它自己就是热点,而不是因为被调用了太多次。

在 DevTools 里,橙色条是 JavaScript,绿色是 V8 内部,蓝色是原生/C++。如果你看到一片橙色里某个函数特别宽,那就是你的优化目标。

火焰图工具:0x

如果你希望一键生成火焰图,可以用 0x 这个工具:

npm install -g 0x

# 生成火焰图
0x slow-app.mjs

运行结束后会自动打开浏览器展示 SVG 火焰图。0x 内部整合了 perf(Linux)或采样逻辑,对 v24 兼容良好。

Clinic.js:诊断套件

NearForm 出品的 Clinic.js 是 Node.js 性能诊断的专业工具集,包含三个子工具:

npm install -g clinic

# Doctor:综合分析,给建议
clinic doctor -- node slow-app.mjs

# Flame:CPU 火焰图
clinic flame -- node slow-app.mjs

# Bubbleprof:异步流分析,定位 I/O 瓶颈
clinic bubbleprof -- node slow-app.mjs

clinic doctor 是我最喜欢的。它跑一遍你的应用,收集 CPU、内存、事件循环等指标,最后生成一个 HTML 报告,直接告诉你 “事件循环延迟过高”、“CPU 占用不均衡” 之类的结论,并给出优化方向。

性能分析的正确姿势

很多新手容易犯的错:

  1. 凭感觉优化。没 profiling 就改代码,改了半天发现瓶颈根本不在这里。
  2. 只看平均值。P50 响应时间好看不代表没问题,P99 可能高得吓人。profiler 里也要关注长尾。
  3. 在开发机测性能。开发机的 CPU、内存、网络和生产完全不同,测量结果没有参考价值。至少要在 staging 环境测。
  4. 一次改太多。优化后重新 profiling,确认真的变快了,再改下一处。

推荐的流程:

建立基准(ab/autocannon) → 运行 profiler → 找到热点 → 优化代码 → 重新测试 → 对比提升