首页 / Node.js 教程 / perf_hooks 与性能测量

Node.js 教程

perf_hooks 与性能测量

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

Node.jsperf_hooks性能测量基准

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 可以订阅 markmeasurefunctiongc 等类型。如果是做长期监控,订阅 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 快几十倍。这种对比在优化前做一遍,能避免凭感觉选错算法。