Node.js 应用应使用 perf_hooks 实时捕获事件循环延迟,通过 PerformanceObserver 监听 'loop' 类型条目,延迟超50ms时用 console.warn 警告,并在生产环境按1%采样;配合 --inspect 在 Chrome DevTools 中录制性能火焰图,聚焦 Self Time 高的函数;优先排查 require 和 fs.readFileSync 等同步操作,对百MB级JSON解析等CPU密集任务才使用 worker_threads。

用 performance API 实时捕获事件循环延迟
Node.js 自带的 perf_hooks 是最轻量、最可靠的检测手段,不需要额外依赖或启动参数。它能直接告诉你“事件循环卡了多久”,而不是靠猜。
在应用入口(比如 app.js 或 server.js)顶部加入:
const { PerformanceObserver, performance } = require('perf_hooks');
const obs = new PerformanceObserver((items) => {
const entry = items.getEntries()[0];
if (entry.duration > 50) {
console.warn(`[EVENT LOOP BLOCKED] ${entry.duration.toFixed(2)}ms`);
}
});
obs.observe({ entryTypes: ['loop'] });
注意:entryTypes: ['loop'] 只在 Node 14.17+ 和 16.13+ 支持;低于此版本请改用 setImmediate + process.hrtime() 手动打点。
- 延迟超过 50ms 就该警觉——这已超出 HTTP 请求处理的安全窗口
- 日志要打在
console.warn而非console.log,避免被其他日志淹没 - 别只在开发环境启用——生产环境也应保留(但建议采样率控制在 1%)
用 --inspect 配合 Chrome DevTools 定位阻塞函数
VSCode 调试器本身不提供火焰图,但通过 --inspect 把 Node 进程暴露给 Chrome DevTools,就能拿到真实调用栈和耗时分布。
修改 .vscode/launch.json,关键点是加 --inspect-brk 和去掉 runtimeArgs 里可能存在的 --require 干扰:
{
"type": "node",
"request": "launch",
"name": "Profile Event Loop",
"program": "${workspaceFolder}/app.js",
"runtimeArgs": ["--inspect-brk"],
"console": "integratedTerminal"
}
启动后,复制底部状态栏的 ws://... 地址,在 Chrome 访问 chrome://inspect → 点击 inspect → 切到 Performance 标签页 → 点击录制(●),操作几秒后停止。
- 重点看
Bottom-Up视图,找Self Time高的函数——不是“被调用最多”的,而是“自己干得最久”的 - 若看到大量
eval或匿名函数占高比例,大概率是同步 JSON 解析、正则匹配或未拆分的大循环 - 避免在录制期间打开 VSCode 的“调试控制台”,它会干扰主线程采样精度
识别常见阻塞源:别只盯代码,先查 require 和 fs.readFileSync
很多阻塞不是写出来的,是引入的。尤其在 require() 时加载了同步初始化逻辑,或模块内部用了 fs.readFileSync 读大文件。
快速筛查方法:
- 全局搜索
fs.readFileSync、child_process.execSync、JSON.parse(.*?\n.*?\n)(超长字符串) - 检查
node_modules中近期安装的包,运行npm ls --prod --depth=0缩小范围 - 在
require()前加console.time('loading ' + path),观察哪个模块加载耗时异常(>100ms)
典型陷阱:dotenv 默认同步读取 .env;node-sass 初始化时编译原生模块;某些 CLI 工具(如 ts-node)在首次 require 时解析全量 TS 配置。
用 worker_threads 移出 CPU 密集任务,但别滥用
不是所有慢操作都适合丢进 Worker。错误地使用反而增加调度开销,让问题更隐蔽。
适用场景很明确:JSON.parse 百 MB 级响应体、Base64 图片解码、加密哈希计算、复杂模板渲染。
不适用场景:fs.readFile(已有异步 API)、setTimeout 控制流、短于 5ms 的计算。
- Worker 启动有 ~10ms 开销,单次任务低于 20ms 基本没收益
- 传参尽量用
postMessage发ArrayBuffer,避免 JSON 序列化大对象 - 别在 Worker 里再
require整个框架(如 Express),只传必要数据和纯函数
真正难的是判断“哪里该加”。与其凭经验,不如先用 perf_hooks 把延迟打点落到具体函数,再决定是否拆。



















