Node.js パフォーマンス分析とチューニング実践:フレームグラフ、メモリリーク、イベントループブロッキングの完全な調査パス

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

Node.js パフォーマンス問題の 3 つの根本原因

種類 症状 ツール
CPU 集中 リクエスト遅延が高い、CPU 100% フレームグラフ、CPU プロファイル
メモリリーク メモリ継続増加、OOM クラッシュ ヒープスナップショット、ヒープ比較
イベントループブロッキング イベントループ遅延が高い、リクエスト待ち行列 monitorEventLoopDelay、blocked-at

1. CPU パフォーマンス分析

1. 組み込み CPU プロファイラ

// CPU プロファイリングを有効にして起動
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 解析 │  │ DB クエリ │      │
      │    └──────────┘  └──────────┘       │
      │         ┌──────────────┐            │
      │         │  正規表現     │ ← 幅 = 時間 │
      │         └──────────────┘            │
      └─────────────────────────────────────┘
      幅 = 関数の時間割合
      高さ = コールスタックの深さ
      色 = ランダム(特別な意味なし)

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 プロファイル

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);
});

2. メモリリーク分析

1. V8 ヒープスナップショット

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. 1 ラウンドの操作を実行(リークシナリオをシミュレート)
// 3. 2 番目のスナップショット
// 4. もう 1 ラウンド実行
// 5. 3 番目のスナップショット
// 6. Snapshot 3 と 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); // 毎回追加される
  }
}

// ✅ 修正:once または手動 off を使用
emitter.once('data', handler);
// または
emitter.on('data', handler);
// 完了時
emitter.off('data', handler);

3. イベントループ監視

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()]);

4. 本番環境の継続的監視

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 一部 OSS 多言語 エラー監視 + パフォーマンス

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();

5. パフォーマンス最適化チェックリスト

CPU 最適化

  • ✅ 大きなファイルには Stream を使用、一括読み込みを避ける
  • ✅ CPU 集約タスクは Worker Thread に移動
  • --max-old-space-size で適切なヒープサイズを設定
  • ✅ 同期 API(readFileSyncexecSync)を避ける

メモリ最適化

  • ✅ キャッシュサイズを制限(LRU)
  • ✅ イベントリスナーとタイマーを適時にクリーンアップ
  • ✅ クロージャが大きなオブジェクトを参照しないようにする
  • ✅ バイナリデータには文字列の代わりに Buffer を使用

イベントループ

  • ✅ ブロッキング操作を Worker Thread に移動
  • ✅ 大きな計算を複数のマイクロタスクに分割
  • ✅ イベントループ遅延を監視し、アラート閾値を設定

まとめ

Node.js パフォーマンス分析の三本柱:フレームグラフで CPU ホットスポットを見つけ、ヒープ比較でメモリリークを見つけ、イベントループ監視でブロッキングを見つける。本番環境では必ず継続的監視(OpenTelemetry + Prometheus + Grafana)を展開し、ユーザーからの苦情を待ってからパフォーマンス問題を発見することのないようにしてください。忘れないでください:パフォーマンス最適化の第一歩はコードの変更ではなく、測定です——データなしでは、すべての最適化は推測に過ぎません。

ブラウザローカルツールを無料で試す →

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