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(
readFileSync、execSync)を避ける
メモリ最適化
- ✅ キャッシュサイズを制限(LRU)
- ✅ イベントリスナーとタイマーを適時にクリーンアップ
- ✅ クロージャが大きなオブジェクトを参照しないようにする
- ✅ バイナリデータには文字列の代わりに Buffer を使用
イベントループ
- ✅ ブロッキング操作を Worker Thread に移動
- ✅ 大きな計算を複数のマイクロタスクに分割
- ✅ イベントループ遅延を監視し、アラート閾値を設定
まとめ
Node.js パフォーマンス分析の三本柱:フレームグラフで CPU ホットスポットを見つけ、ヒープ比較でメモリリークを見つけ、イベントループ監視でブロッキングを見つける。本番環境では必ず継続的監視(OpenTelemetry + Prometheus + Grafana)を展開し、ユーザーからの苦情を待ってからパフォーマンス問題を発見することのないようにしてください。忘れないでください:パフォーマンス最適化の第一歩はコードの変更ではなく、測定です——データなしでは、すべての最適化は推測に過ぎません。
ブラウザローカルツールを無料で試す →
#Node.js#性能分析#火焰图#内存泄漏#性能优化