Node.jsパフォーマンス最適化:イベントループ、クラスタリング、最適化テクニック2026年版
Node.js 22 LTS・Node.js 24におけるイベントループの仕組み、クラスターモジュールによるマルチコアスケーリング、Worker Threadsの活用、メモリ最適化まで、本番環境で必要なパフォーマンスチューニング手法を詳しく解説します。

Node.jsのパフォーマンス最適化において、イベントループの理解は最も重要な基盤となります。イベントループは、すべての非同期I/Oを処理するシングルスレッドメカニズムであり、Node.js 22 LTSやNode.js 24を運用する本番システムにおいて、イベントループの管理が不適切な場合、レイテンシスパイク、コネクションドロップ、高負荷時のカスケード障害の主要因となります。
本記事では、Node.jsパフォーマンスの3つの柱について解説します。イベントループの内部構造、クラスターとWorker Threadsによるマルチコアスケーリング、そして高スループットシステムで使用される実践的な最適化パターンです。
イベントループは厳密なフェーズ順序でコールバックを処理します:timers、pending callbacks、idle/prepare、poll、check、close callbacksの順です。いずれかのフェーズをブロックすると、アプリケーション全体が停止します。performance.eventLoopUtilization()によるイベントループ使用率の監視は、Node.jsパフォーマンス問題に対する最も効果的な診断ツールです。
Node.jsイベントループのリクエスト処理メカニズム
イベントループは単純なキューではなく、6つの異なるフェーズで動作します。各フェーズは特定のカテゴリのコールバックを担当しており、このフェーズアーキテクチャを理解することで、特定のパターンがレイテンシを引き起こす理由を説明できます。
// Demonstrating phase execution order
const fs = require('fs');
// Phase 1: Timers — executes setTimeout/setInterval callbacks
setTimeout(() => console.log('1. Timer phase'), 0);
// Phase 4: Poll — executes I/O callbacks
fs.readFile(__filename, () => {
console.log('2. Poll phase (I/O callback)');
// Phase 5: Check — executes setImmediate callbacks
setImmediate(() => console.log('3. Check phase (setImmediate)'));
// Phase 1 again: Timer scheduled from within I/O
setTimeout(() => console.log('4. Timer phase (from I/O)'), 0);
});
// Microtask — runs between every phase transition
Promise.resolve().then(() => console.log('Microtask: Promise'));
process.nextTick(() => console.log('Microtask: nextTick'));出力順序がフェーズの優先度を示しています。nextTickはPromiseよりも先に実行され、PromiseはTimerよりも先に実行されます。setImmediateはI/Oコールバックのcheckフェーズで常に発火します。この順序は、レイテンシに敏感なリクエストハンドラを設計する際に重要です。
本番環境におけるイベントループブロッキングの検出
イベントループのブロッキングは、平均レスポンスタイムが悪化するよりもずっと前に、p99レイテンシの上昇として現れます。Node.js 16以降で安定版となったperformance.eventLoopUtilization() APIが、最も信頼性の高い検出メカニズムを提供します。
// Production-grade event loop monitoring
const { performance, monitorEventLoopDelay } = require('perf_hooks');
// High-resolution event loop delay histogram
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();
// Track utilization over intervals
let previous = performance.eventLoopUtilization();
setInterval(() => {
const current = performance.eventLoopUtilization(previous);
previous = performance.eventLoopUtilization();
const metrics = {
// Ratio of time the loop spent active vs idle (0-1)
utilization: current.utilization.toFixed(3),
// Delay percentiles in milliseconds
p50: (histogram.percentile(50) / 1e6).toFixed(2),
p99: (histogram.percentile(99) / 1e6).toFixed(2),
max: (histogram.max / 1e6).toFixed(2),
};
// Alert when utilization exceeds 70% or p99 > 100ms
if (current.utilization > 0.7 || histogram.percentile(99) > 100e6) {
console.warn('EVENT_LOOP_SATURATED', metrics);
}
histogram.reset();
}, 5000);使用率が0.7(70%)を超えると、イベントループはI/Oを待機する時間よりもコールバックの実行に多くの時間を費やしていることを示します。この閾値では、受信接続がキューイングされ始め、テールレイテンシが指数関数的に増加します。
一般的なイベントループブロッカーとその解決策
本番環境のNode.jsアプリケーションにおけるイベントループブロッキングの大部分は、大きなペイロードに対する同期的なJSON操作、リクエストハンドラ内のCPU集約的な計算、そして制限のない正規表現の3つのパターンに起因します。
// Anti-patterns and their solutions
// PROBLEM: JSON.parse blocks on large payloads
const largePayload = Buffer.alloc(50 * 1024 * 1024); // 50MB
// JSON.parse(largePayload.toString()); // Blocks event loop 200-500ms
// SOLUTION: Stream-parse large JSON with a streaming parser
const { Transform } = require('stream');
const JSONStream = require('jsonstream2');
function processLargeJSON(readableStream) {
return new Promise((resolve, reject) => {
const results = [];
readableStream
.pipe(JSONStream.parse('items.*')) // Stream-parse array items
.on('data', (item) => results.push(item))
.on('end', () => resolve(results))
.on('error', reject);
});
}
// PROBLEM: Synchronous crypto in request path
// const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512');
// SOLUTION: Use async variant
const crypto = require('crypto');
async function hashPassword(password, salt) {
return new Promise((resolve, reject) => {
crypto.pbkdf2(password, salt, 100000, 64, 'sha512', (err, key) => {
if (err) reject(err);
else resolve(key.toString('hex'));
});
});
}非同期のpbkdf2バリアントは、CPU処理をlibuvスレッドプールにオフロードし、イベントループを他のリクエスト処理のために解放します。この1つの変更だけで、あるフィンテック企業の本番環境でp99レイテンシが900msから120msに削減されたと報告されています。
クラスターモジュールによるCPUコアのスケーリング
単一のNode.jsプロセスは1つのCPUコアを使用します。16コアの本番サーバーでは、利用可能な計算リソースの93%がアイドル状態になります。組み込みのclusterモジュールは、単一のポートを共有するワーカープロセスを生成し、すべてのコアに受信接続を分散します。
// Production clustering with graceful shutdown
const cluster = require('cluster');
const os = require('os');
const process = require('process');
const WORKER_COUNT = parseInt(process.env.WORKERS) || os.cpus().length;
if (cluster.isPrimary) {
console.log(`Primary ${process.pid} starting ${WORKER_COUNT} workers`);
// Fork workers for each CPU core
for (let i = 0; i < WORKER_COUNT; i++) {
cluster.fork();
}
// Restart crashed workers automatically
cluster.on('exit', (worker, code, signal) => {
if (!worker.exitedAfterDisconnect) {
console.error(`Worker ${worker.process.pid} died (${signal || code}). Restarting...`);
cluster.fork();
}
});
// Graceful shutdown on SIGTERM
process.on('SIGTERM', () => {
console.log('Primary received SIGTERM. Shutting down workers...');
for (const id in cluster.workers) {
cluster.workers[id].disconnect();
}
});
} else {
// Worker process — start the actual HTTP server
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end(`Handled by worker ${process.pid}\n`);
});
server.listen(3000, () => {
console.log(`Worker ${process.pid} listening on port 3000`);
});
// Graceful shutdown for individual worker
process.on('SIGTERM', () => {
server.close(() => process.exit(0));
});
}各ワーカーは独自のV8アイソレートで実行され、別々のメモリを持ちます。ワーカー間で状態を共有することはできません。セッションデータ、キャッシュ、アプリケーションステートはRedisやPostgreSQLなどの外部ストアに置く必要があります。この分離は耐障害性も提供します。1つのワーカーのクラッシュは他のワーカーに影響しません。
Node.js / NestJSの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
Worker Threadsを使用したCPU集約的タスクの処理
clusterモジュールはプロセス全体を複製します。Worker Threadsは、Node.js 10で導入されNode.js 12以降で安定版となったもので、同じプロセス内の並列スレッドでJavaScriptを実行します。SharedArrayBufferを通じてメモリを共有し、構造化クローンを介してデータを転送します。
この区別は重要です。I/OバウンドのHTTPサーバーをコア全体にスケーリングするにはクラスターを使用し、イベントループからCPUバウンドの操作をオフロードするにはWorker Threadsを使用します。
// Reusable worker thread pool for CPU tasks
const { Worker } = require('worker_threads');
const os = require('os');
class WorkerPool {
constructor(workerScript, poolSize = os.cpus().length) {
this.workers = [];
this.queue = [];
for (let i = 0; i < poolSize; i++) {
this.workers.push({ busy: false, worker: new Worker(workerScript) });
}
}
execute(taskData) {
return new Promise((resolve, reject) => {
const available = this.workers.find(w => !w.busy);
if (available) {
this._runTask(available, taskData, resolve, reject);
} else {
// Queue task until a worker is free
this.queue.push({ taskData, resolve, reject });
}
});
}
_runTask(entry, taskData, resolve, reject) {
entry.busy = true;
entry.worker.postMessage(taskData);
const onMessage = (result) => {
entry.busy = false;
cleanup();
resolve(result);
this._processQueue();
};
const onError = (err) => {
entry.busy = false;
cleanup();
reject(err);
this._processQueue();
};
const cleanup = () => {
entry.worker.removeListener('message', onMessage);
entry.worker.removeListener('error', onError);
};
entry.worker.on('message', onMessage);
entry.worker.on('error', onError);
}
_processQueue() {
if (this.queue.length === 0) return;
const available = this.workers.find(w => !w.busy);
if (available) {
const { taskData, resolve, reject } = this.queue.shift();
this._runTask(available, taskData, resolve, reject);
}
}
}
module.exports = { WorkerPool };// Worker thread for CPU-intensive image processing
const { parentPort } = require('worker_threads');
const sharp = require('sharp');
parentPort.on('message', async ({ inputPath, width, height }) => {
const result = await sharp(inputPath)
.resize(width, height)
.webp({ quality: 80 })
.toBuffer();
parentPort.postMessage({ size: result.length, buffer: result });
});ワーカープールは起動時にスレッドを事前生成し、リクエスト間で再利用します。これにより、リクエストごとに新しいワーカースレッドを作成する30〜50msのオーバーヘッドを回避できます。画像処理、PDF生成、データ変換の場合、このパターンにより、持続的なCPU負荷の下でもメインイベントループのレイテンシを5ms以下に保つことができます。
メモリ最適化とガベージコレクションチューニング
V8はヒープをyoung generation(短命オブジェクト)とold generation(長命オブジェクト)に分割します。ほとんどのパフォーマンス問題は、young generationでの過剰なアロケーション(頻繁なminor GCポーズの原因)、またはold generationを成長させるメモリリーク(major GCポーズが可視的なレイテンシスパイクを引き起こすまで)に起因します。
// Patterns that reduce GC pressure
// ANTI-PATTERN: Creating objects in hot loops
function processItemsBad(items) {
return items.map(item => ({
id: item.id,
name: item.name.trim(),
score: calculateScore(item), // New object per iteration
metadata: { processed: true, timestamp: Date.now() }
}));
}
// OPTIMIZED: Reuse buffers and minimize allocations
const reusableBuffer = Buffer.alloc(4096);
function processItemsGood(items, output) {
// Reuse the output array instead of creating new one
output.length = 0;
for (let i = 0; i < items.length; i++) {
// Mutate in place when safe to do so
output.push(items[i].id);
}
return output;
}
// Monitor heap usage for leak detection
function checkMemory() {
const used = process.memoryUsage();
return {
heapUsedMB: Math.round(used.heapUsed / 1024 / 1024),
heapTotalMB: Math.round(used.heapTotal / 1024 / 1024),
externalMB: Math.round(used.external / 1024 / 1024),
rsssMB: Math.round(used.rss / 1024 / 1024),
};
}
// V8 flags for production GC tuning
// node --max-old-space-size=4096 --max-semi-space-size=128 app.js
// --max-old-space-size: Set old generation limit (default ~1.7GB)
// --max-semi-space-size: Increase young generation (default 16MB)--max-semi-space-sizeをデフォルトの16MBから64〜128MBに増やすことで、アロケーション率の高いアプリケーションのminor GC頻度を削減できます。これは、メモリとGCポーズ頻度の低下のトレードオフです。8GB以上のRAMを持つサーバーでは、このトレードオフは合理的です。
Node.js 24におけるOpenTelemetryによる本番監視
OpenTelemetryは2026年にNode.jsの標準的なインストルメンテーションフレームワークとなりました。Node.js 24ランタイムには、CPUプロファイルにインラインキャッシュデータを含む改善されたプロファイリングサポートが含まれており、パフォーマンス分析がより正確になっています。
// OpenTelemetry setup for Node.js performance monitoring
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const sdk = new NodeSDK({
metricReader: new PrometheusExporter({ port: 9464 }),
instrumentations: [
getNodeAutoInstrumentations({
// Instrument HTTP, Express, DNS, fs, and more
'@opentelemetry/instrumentation-fs': { enabled: false }, // Too noisy
}),
],
});
sdk.start();
// Custom event loop lag metric
const { metrics } = require('@opentelemetry/api');
const meter = metrics.getMeter('app');
const eventLoopLag = meter.createHistogram('nodejs.event_loop.lag', {
description: 'Event loop lag in milliseconds',
unit: 'ms',
});
// Report event loop lag every second
const { monitorEventLoopDelay } = require('perf_hooks');
const h = monitorEventLoopDelay({ resolution: 10 });
h.enable();
setInterval(() => {
eventLoopLag.record(h.percentile(99) / 1e6);
h.reset();
}, 1000);このセットアップにより、イベントループラグ、HTTPリクエスト時間、DNS解決時間がPrometheusメトリクスとしてエクスポートされます。イベントループラグのp99 > 100msにアラートを設定することで、ユーザーが気づく前にパフォーマンス低下を検知できます。
各クラスターワーカーは独自のlibuvスレッドプール(デフォルト4スレッド)を持ちます。16コアのマシンで16のクラスターワーカーを実行すると、64のlibuvスレッドがCPU時間を競合します。クラスター環境ではUV_THREADPOOL_SIZEをワーカーあたり2〜4に設定し、CPUタスクを処理するWorker Threads用にコアを予約してください。
クラスター、Worker Threads、外部スケーリングの選択
適切なスケーリング戦略はワークロードプロファイルに依存します。実際の本番パターンに基づく決定マトリクスは以下の通りです。
| シナリオ | 戦略 | 理由 | |----------|------|------| | HTTPのAPI、主にI/O | クラスターモジュール(コアあたり1ワーカー) | 接続スループットを最大化 | | 画像・動画処理 | Worker Threadプール(4〜8スレッド) | イベントループの応答性を維持 | | CPU負荷の高いデータパイプライン | Worker Threads + SharedArrayBuffer | ゼロコピーデータ共有 | | 大規模マイクロサービス | Kubernetesポッド(シングルプロセスコンテナ) | オーケストレータがスケーリングを管理 | | I/O + CPUの混合 | クラスター + ワーカーごとのスレッドプール | 各ワーカーがCPUをスレッドにオフロード |
Kubernetes上のコンテナ化デプロイメントでは、コンテナごとに単一のNode.jsプロセスを実行する(クラスタリングなし)方が、よりシンプルで予測可能です。オーケストレータが水平スケーリング、ヘルスチェック、ローリングリスタートを処理します。クラスタリングは、単一マシンがコア使用率を最大化する必要があるベアメタルやVMで実行する場合に価値があります。
2025年にリリースされたNode.js 24は、V8 12.4を搭載し、APIワークロードで8〜12%のスループット向上を実現します。組み込みのfetch()はUndici 7.0を使用し、デフォルトでHTTP/2およびHTTP/3をサポートします。パーミッションモデルにより、ワーカーごとのファイルシステムおよびネットワークアクセスのロックダウンも可能になり、多層防御を実現します。
本番環境向け実践的最適化チェックリスト
これらのテクニックをインパクトの大きい順に適用することで(イベントループの健全性を最初に、次にスケーリング、そして微調整)、最小限のリスクで測定可能な改善を得られます。
- イベントループ使用率を継続的に監視 —
performance.eventLoopUtilization()で70%の閾値でアラート - 最適化の前にプロファイリング —
node --profまたはOpenTelemetryトレースを使用して、想定ではなく実際のボトルネックを特定 - 同期操作をメインスレッドから移動 —
crypto.pbkdf2、大きなペイロードのJSON.parse、画像処理はすべてWorker Threadsで処理 - ワークロードに基づいて
UV_THREADPOOL_SIZEを設定 — デフォルトの4はDNSやファイルI/Oが多いアプリには少なすぎ、クラスター構成では多すぎる - 大きなデータにはストリーミングを使用 —
JSONStream、csv-parser、Node.jsのstream.pipeline()で大きなペイロードのメモリスパイクを防止 - 起動時にワーカープールをウォームアップ — CPU集約的エンドポイントへの最初のリクエストでのコールドスタートレイテンシを回避
- デプロイメントに合わせてV8ヒープフラグを調整 — 利用可能なメモリとアロケーションパターンに基づいて
--max-old-space-sizeと--max-semi-space-sizeを設定 - グレースフルシャットダウンを実装 — SIGTERMで接続をドレイン、データベースプールを閉じ、処理中のリクエストをプロセス終了前に完了させる
今すぐ練習を始めましょう!
面接シミュレーターと技術テストで知識をテストしましょう。
まとめ
2026年のNode.jsパフォーマンスは、3つの基本に集約されます。
- イベントループはスループットの単一障害点であり、使用率の監視とブロッカーの排除がレイテンシに最も大きな影響を与えます
- クラスタリングはI/Oバウンドのワークロードをコア全体にスケーリングし、Worker Threadsはイベントループからのcpuバウンドタスクのオフロードに使用されます。両方を組み合わせることで、最大のハードウェア使用率を実現できます
- OpenTelemetryとV8プロファイリングツールによる本番監視は、推測をデータに変えます。イベントループラグのp99とヒープ成長率にアラートを設定してください
- Node.js 24はV8 12.4のパフォーマンス向上、Undici 7による安定したHTTP/3、成熟したパーミッションモデルを提供します。Node.js 20以前からのアップグレードで測定可能なスループット改善が得られます
- 既存のNode.jsアプリケーションで
performance.eventLoopUtilization()を使用することから始めてください。多くの場合、利用可能な最もインパクトの高い最適化が明らかになります
タグ
共有
関連記事

NestJS + Prisma:Node.js のためのモダンなバックエンドスタック
NestJS と Prisma によるモダンなバックエンド API 構築の完全ガイドです。セットアップ、モデル、サービス、トランザクション、ベストプラクティスを解説します。

Node.jsバックエンド面接質問:完全ガイド2026
Node.jsバックエンド面接で最も頻出の25問。Event Loop、async/await、Streams、クラスタリング、パフォーマンスを詳細な回答で解説します。

NestJS: 本格的なREST APIの構築ガイド
NestJSで本格的なREST APIを構築するための完全ガイドです。コントローラー、サービス、モジュール構成、class-validatorによるバリデーション、エラーハンドリングを実践的に解説します。