perf_hooks 与性能测量
本教程共 76 篇 · 第 37 篇 · 更新于 2026-07-25 · 约 6 分钟阅读
37. perf_hooks 与性能测量
本节目标:用 perf_hooks 测量代码性能,找出性能瓶颈。
“这段代码慢,帮我优化一下。“——很多时候你听到的 “慢” 只是一个模糊的感受。真正动手之前,你得先知道慢在哪里、慢了多少。Node.js 内置的 perf_hooks 模块就是干这个的,不需要装任何第三方包。
performance.now():高精度计时
最基础的用法,performance.now() 返回从进程启动到当前时刻的毫秒数,精度达到微秒级(小数点后三位),比 Date.now() 更适合测量代码耗时。
import { performance } from 'node:perf_hooks';
const start = performance.now();
let sum = 0;
for (let i = 0; i < 1e7; i++) {
sum += i;
}
const end = performance.now();
console.log(`耗时: ${(end - start).toFixed(3)} ms`);
Date.now() 的精度只有毫秒,而且受系统时间调整的影响(比如 NTP 同步会拨时钟)。performance.now() 基于单调时钟,只往前走,不会被系统时间干扰。
Tip自 v8.5.0 起
perf_hooks就是稳定模块了,v24 LTS 里可以放心用。
mark 和 measure:给代码打标记
如果代码里有多段逻辑要分别计时,performance.now() 需要你自己管理变量。mark() 和 measure() 提供了更系统的方案:
import { performance } from 'node:perf_hooks';
performance.mark('db-start');
await fetchUserFromDatabase();
performance.mark('db-end');
performance.mark('cache-start');
await fetchUserFromCache();
performance.mark('cache-end');
// 创建 measure,自动计算两个 mark 之间的时间差
performance.measure('数据库查询', 'db-start', 'db-end');
performance.measure('缓存查询', 'cache-start', 'cache-end');
// 查看结果
const measures = performance.getEntriesByType('measure');
for (const m of measures) {
console.log(`${m.name}: ${m.duration.toFixed(3)} ms`);
}
// 清理,避免内存泄漏
performance.clearMarks();
performance.clearMeasures();
mark 是在时间轴上打一个点,measure 是计算两个点之间的距离。所有标记和测量都保存在 performance 实例的内部时间线里,可以通过 API 查询和清理。
PerformanceObserver:异步监听
手动调用 getEntriesByType() 去轮询结果比较麻烦。PerformanceObserver 可以在测量完成时自动收到通知:
import { performance, PerformanceObserver } from 'node:perf_hooks';
const obs = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`[${entry.entryType}] ${entry.name}: ${entry.duration.toFixed(3)} ms`);
}
});
obs.observe({ entryTypes: ['measure'] });
// 之后创建的 measure 会自动触发上面的回调
performance.mark('a');
setTimeout(() => {
performance.mark('b');
performance.measure('延迟测试', 'a', 'b');
obs.disconnect(); // 用完记得断开
}, 100);
entryTypes 可以订阅 mark、measure、function、gc 等类型。如果是做长期监控,订阅 measure 类型然后在回调里把指标打到日志或时序数据库,就能持续跟踪性能变化。
监控事件循环延迟
事件循环是 Node.js 的心脏。如果它被打堵了,所有异步操作都会变慢。perf_hooks 提供了 monitorEventLoopDelay(),专门用来量化这个问题:
import { monitorEventLoopDelay } from 'node:perf_hooks';
const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();
// 跑你的应用...
setTimeout(() => {
histogram.disable();
console.log(`最小延迟: ${histogram.min} ns`);
console.log(`最大延迟: ${histogram.max} ns`);
console.log(`平均延迟: ${histogram.mean.toFixed(0)} ns`);
console.log(`P99 延迟: ${histogram.percentile(99).toFixed(0)} ns`);
}, 5000);
resolution 是采样间隔(毫秒)。histogram 会记录每次事件循环 tick 的延迟,输出纳秒级的统计结果。
在生产环境里,如果 P99 事件循环延迟超过 100ms,说明有同步代码在阻塞事件循环,需要排查。常见的罪魁祸首包括:同步读取大文件、复杂的正则表达式、没有分页的大数据遍历。
给 HTTP 接口加性能埋点
实际项目里,最常见的性能测量需求是统计每个接口的响应时间。用 Express 中间件配合 perf_hooks 可以很干净地实现:
import express from 'express';
import { performance, PerformanceObserver } from 'node:perf_hooks';
const app = express();
// 自动记录所有 measure 到日志
const obs = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name.startsWith('api:')) {
console.log(JSON.stringify({
time: new Date().toISOString(),
route: entry.name.slice(4),
durationMs: Math.round(entry.duration * 100) / 100,
}));
}
}
});
obs.observe({ entryTypes: ['measure'] });
// 中间件:在请求开始时打 mark,响应结束时计算 measure
app.use((req, res, next) => {
const markName = `${req.method} ${req.path}@${Date.now()}`;
performance.mark(`${markName}-start`);
res.on('finish', () => {
performance.mark(`${markName}-end`);
performance.measure(
`api:${req.method} ${req.route?.path || req.path}`,
`${markName}-start`,
`${markName}-end`
);
performance.clearMarks(`${markName}-start`);
performance.clearMarks(`${markName}-end`);
});
next();
});
app.get('/fast', (req, res) => res.json({ ok: true }));
app.get('/slow', (req, res) => {
setTimeout(() => res.json({ ok: true }), 200);
});
app.listen(3000, () => console.log('监听 3000'));
每个请求结束后,都会输出一行结构化日志,包含路由和耗时。把这些日志收集到 Elasticsearch/Loki 里,就能做接口级性能监控和告警。
与 console.time 的区别
Node.js 也提供了 console.time() 和 console.timeEnd(),用法更简单:
console.time('排序');
array.sort((a, b) => a - b);
console.timeEnd('排序'); // 排序: 23.456ms
区别主要在于:
console.time只能输出到控制台,无法程序化读取结果。perf_hooks的 measure 可以存入变量、传给 Observer、持久化到日志。perf_hooks支持更丰富的 entry 类型(包括 GC、事件循环延迟等)。
临时调试用 console.time 够方便,但要集成到监控体系里,还是得用 perf_hooks。
写一个简易 benchmark 工具
如果你想对比两种实现的性能,可以封装一个简易的 benchmark 函数:
import { performance } from 'node:perf_hooks';
function benchmark(name, fn, iterations = 1000) {
// 预热,让 V8 JIT 编译稳定
for (let i = 0; i < 10; i++) fn();
const times = [];
for (let i = 0; i < iterations; i++) {
const start = performance.now();
fn();
const end = performance.now();
times.push(end - start);
}
times.sort((a, b) => a - b);
const sum = times.reduce((a, b) => a + b, 0);
return {
name,
iterations,
total: sum.toFixed(2),
avg: (sum / iterations).toFixed(4),
min: times[0].toFixed(4),
max: times[times.length - 1].toFixed(4),
p50: times[Math.floor(iterations * 0.5)].toFixed(4),
p99: times[Math.floor(iterations * 0.99)].toFixed(4),
};
}
// 对比两种数组去重方式
const arr = Array.from({ length: 10000 }, () => Math.floor(Math.random() * 5000));
const r1 = benchmark('Set 去重', () => [...new Set(arr)]);
const r2 = benchmark('filter+indexOf 去重', () => arr.filter((v, i) => arr.indexOf(v) === i));
console.table([r1, r2]);
跑出来的结果通常会显示 Set 方案比 filter+indexOf 快几十倍。这种对比在优化前做一遍,能避免凭感觉选错算法。