性能分析 Profiling
本教程共 76 篇 · 第 67 篇 · 更新于 2026-07-25 · 约 6 分钟阅读
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
这里清晰地展示了调用链:pbkdf2Sync → pbkdf2 → PBKDF2(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)是把采样结果可视化的一种方式。每一层是一个函数,宽度代表该函数在采样中出现的比例。看火焰图的技巧:
- 找最宽的塔:宽就是耗时长,优化它收益最大。
- 从下往上看:底部是调用栈的根,往上是被调用的函数。
- 关注 “平顶”:如果某个函数的宽度几乎和它的父函数一样宽,说明它自己就是热点,而不是因为被调用了太多次。
在 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 占用不均衡” 之类的结论,并给出优化方向。
性能分析的正确姿势
很多新手容易犯的错:
- 凭感觉优化。没 profiling 就改代码,改了半天发现瓶颈根本不在这里。
- 只看平均值。P50 响应时间好看不代表没问题,P99 可能高得吓人。profiler 里也要关注长尾。
- 在开发机测性能。开发机的 CPU、内存、网络和生产完全不同,测量结果没有参考价值。至少要在 staging 环境测。
- 一次改太多。优化后重新 profiling,确认真的变快了,再改下一处。
推荐的流程:
建立基准(ab/autocannon) → 运行 profiler → 找到热点 → 优化代码 → 重新测试 → 对比提升