Node.js 效能分析與調優實戰:火焰圖、記憶體洩漏與事件迴圈阻塞的完整排查路徑

性能优化(更新於 2026年6月2日)

Node.js 效能問題的三類根源

型別 症狀 工具
CPU 密集 請求延遲高、CPU 100% 火焰圖、CPU Profile
記憶體洩漏 記憶體持續增長、OOM 崩潰 Heap Snapshot、Heap Diff
事件迴圈阻塞 事件迴圈延遲高、請求排隊 monitorEventLoopDelay、blocked-at

一、CPU 效能分析

1. 內建 CPU Profiler

// 啟動時開啟 CPU profiling
node --prof app.js

// 執行壓測
autocannon -c 100 -d 10 http://localhost:3000/api/users

// 處理 isolate 日誌
node --prof-process isolate-0xnnnnnn-v8.log > profile.txt

2. 使用 0x 生成火焰圖

# 安裝
npm install -g 0x

# 執行並生成火焰圖
0x app.js

# 壓測觸發
autocannon -c 100 -d 10 http://localhost:3000/api/users

# Ctrl+C 停止,自動開啟火焰圖 HTML

火焰圖解讀:

      ┌─────────────────────────────────────┐
      │           HTTP 處理                  │
      │    ┌──────────┐  ┌──────────┐       │
      │    │ JSON 解析 │  │ 資料庫查詢│      │
      │    └──────────┘  └──────────┘       │
      │         ┌──────────────┐            │
      │         │  正則匹配     │ ← 寬 = 耗時 │
      │         └──────────────┘            │
      └─────────────────────────────────────┘
      寬度 = 函數耗時佔比
      高度 = 呼叫棧深度
      顏色 = 隨機(無特殊含義)

3. clinic.js 全套診斷

# 安裝
npm install -g clinic

# CPU 分析(火焰圖)
clinic flame -- node app.js

# 事件迴圈分析(氣泡圖)
clinic bubbleprof -- node app.js

# 醫生模式(自動診斷)
clinic doctor -- node app.js

4. 程式化 CPU Profile

const { Session } = require('inspector');

async function cpuProfile(durationMs = 5000) {
  const session = new Session();
  session.connect();

  session.post('Profiler.enable');
  session.post('Profiler.start');

  await new Promise(resolve => setTimeout(resolve, durationMs));

  return new Promise(resolve => {
    session.post('Profiler.stop', (err, { profile }) => {
      session.disconnect();
      resolve(profile);
    });
  });
}

// 使用
app.get('/debug/cpu-profile', async (req, res) => {
  const profile = await cpuProfile(3000);
  res.json(profile);
});

二、記憶體洩漏分析

1. V8 Heap Snapshot

const v8 = require('v8');

// 手動生成堆快照
app.get('/debug/heap-snapshot', (req, res) => {
  const snapshot = v8.writeHeapSnapshot();
  res.json({ file: snapshot });
});

2. Chrome DevTools 遠端除錯

# 啟動時開啟 inspect
node --inspect=0.0.0.0:9229 app.js

# Chrome 開啟 chrome://inspect → 點選 inspect
# → Memory 面板 → Take Heap Snapshot

3. 堆對比法(最有效)

// 步驟:
// 1. 應用啟動後,拍第一個快照(基準)
// 2. 執行一輪操作(模擬洩漏場景)
// 3. 拍第二個快照
// 4. 再執行一輪操作
// 5. 拍第三個快照
// 6. 對比 Snapshot 3 vs Snapshot 2

// 自動化堆對比
const v8 = require('v8');
const fs = require('fs');

class HeapTracker {
  private snapshots: string[] = [];

  takeSnapshot(label: string) {
    const file = v8.writeHeapSnapshot();
    this.snapshots.push(file);
    console.log(`[${label}] Snapshot: ${file}`);
  }

  async compare() {
    if (this.snapshots.length < 2) return;
    const latest = this.snapshots[this.snapshots.length - 1];
    const previous = this.snapshots[this.snapshots.length - 2];
    // 用 Chrome DevTools 載入對比
    console.log(`Compare: ${previous} vs ${latest}`);
  }
}

4. 常見洩漏模式

// ❌ 模式 1:全域陣列持續增長
const cache = [];
app.get('/api/data', (req, res) => {
  cache.push(largeObject); // 永遠不清理
  res.json(cache);
});

// ✅ 修復:限制大小 + LRU
const cache = new Map();
const MAX_CACHE = 1000;

function addToCache(key, value) {
  if (cache.size >= MAX_CACHE) {
    const firstKey = cache.keys().next().value;
    cache.delete(firstKey);
  }
  cache.set(key, value);
}

// ❌ 模式 2:閉包引用大物件
function createHandler() {
  const hugeData = loadHugeData(); // 100MB

  return (req, res) => {
    // hugeData 被閉包引用,永遠不會被 GC
    const result = hugeData.filter(/* ... */);
    res.json(result);
  };
}

// ✅ 修復:只保留需要的引用
function createHandler() {
  const index = buildIndex(loadHugeData()); // 只保留索引

  return (req, res) => {
    const result = index.lookup(req.query.q);
    res.json(result);
  };
}

// ❌ 模式 3:事件監聽器未移除
class EventEmitter {
  on(event, handler) {
    this.listeners.push(handler); // 每次 on 都新增
  }
}

// ✅ 修復:使用 once 或手動 off
emitter.once('data', handler);
// 或
emitter.on('data', handler);
// 完成後
emitter.off('data', handler);

三、事件迴圈監控

1. monitorEventLoopDelay

const { monitorEventLoopDelay } = require('perf_hooks');

const h = monitorEventLoopDelay({ resolution: 20 });
h.enable();

setInterval(() => {
  console.log({
    p50: h.p50 / 1e6,    // P50 延遲(ms)
    p90: h.p90 / 1e6,    // P90 延遲
    p99: h.p99 / 1e6,    // P99 延遲
    min: h.min / 1e6,
    max: h.max / 1e6,
  });
}, 5000);

h.disable();

2. blocked-at:自動檢測阻塞

npm install blocked-at
const blocked = require('blocked-at');

blocked((stack, delay) => {
  console.error(`事件迴圈阻塞 ${delay}ms`);
  console.error('阻塞堆疊:', stack);
}, { threshold: 50 }); // 超過 50ms 報警

3. 常見阻塞操作

// ❌ 同步檔案讀取
const data = fs.readFileSync('/large-file.json');

// ✅ 非同步讀取
const data = await fs.promises.readFile('/large-file.json');

// ❌ 同步加密
const hash = crypto.createHash('sha256').update(data).digest('hex');

// ✅ 如果資料大,使用 Stream
const hash = crypto.createHash('sha256');
fs.createReadStream('/large-file').pipe(hash);

// ❌ JSON.parse 大字串
const data = JSON.parse(hugeString);

// ✅ 流式 JSON 解析
const { chain } = require('stream-chain');
const { parser } = require('stream-json');
const pipeline = chain([fs.createReadStream('/large.json'), parser()]);

四、生產環境持續監控

1. Prometheus + Grafana

const promClient = require('prom-client');
const register = new promClient.Registry();

// 預設指標(CPU、記憶體、GC)
promClient.collectDefaultMetrics({ register });

// 自訂指標
const httpRequestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP 請求耗時',
  labelNames: ['method', 'route', 'status'],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 5],
  registers: [register],
});

// 中介層
app.use((req, res, next) => {
  const start = process.hrtime.bigint();
  res.on('finish', () => {
    const duration = Number(process.hrtime.bigint() - start) / 1e9;
    httpRequestDuration
      .labels(req.method, req.route?.path, res.statusCode)
      .observe(duration);
  });
  next();
});

// 暴露指標
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

2. APM 方案對比

方案 開源 語言 特點
OpenTelemetry 多語言 標準,可對接任意後端
Jaeger Go 分散式追蹤
New Relic 多語言 全功能 APM
Datadog 多語言 全功能 APM
Sentry 部分開源 多語言 錯誤監控 + 效能

3. OpenTelemetry 整合

const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: 'http://localhost:4318/v1/traces',
  }),
  metricExporter: new OTLPMetricExporter({
    url: 'http://localhost:4318/v1/metrics',
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

五、效能最佳化清單

CPU 最佳化

  • ✅ 用 Stream 處理大檔案,避免一次性載入
  • ✅ 計算密集任務移到 Worker Thread
  • ✅ 使用 --max-old-space-size 合理設定堆大小
  • ✅ 避免同步 API(readFileSyncexecSync

記憶體最佳化

  • ✅ 限制快取大小(LRU)
  • ✅ 及時清理事件監聽器和定時器
  • ✅ 避免閉包引用大物件
  • ✅ 用 Buffer 代替字串處理二進位

事件迴圈

  • ✅ 阻塞操作移到 Worker Thread
  • ✅ 大計算拆分為多個微任務
  • ✅ 監控事件迴圈延遲,設定告警閾值

總結

Node.js 效能分析的三板斧:火焰圖找 CPU 熱點、堆對比找記憶體洩漏、事件迴圈監控找阻塞。生產環境務必部署持續監控(OpenTelemetry + Prometheus + Grafana),不要等到使用者投訴才發現效能問題。記住:效能最佳化的第一步不是改程式碼,而是測量——沒有資料,一切最佳化都是猜測。

本站提供瀏覽器本地工具,免註冊即可試用 →

#Node.js#性能分析#火焰图#内存泄漏#性能优化