# Node.jsパフォーマンス最適化:イベントループ、クラスタリング、最適化テクニック2026年版 > Node.js 22 LTS・Node.js 24におけるイベントループの仕組み、クラスターモジュールによるマルチコアスケーリング、Worker Threadsの活用、メモリ最適化まで、本番環境で必要なパフォーマンスチューニング手法を詳しく解説します。 - Published: 2026-04-25 - Updated: 2026-04-25 - Author: SharpSkill - Tags: node.js, performance, event-loop, clustering, optimization, worker-threads - Reading time: 9 min --- 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つの異なるフェーズで動作します。各フェーズは特定のカテゴリのコールバックを担当しており、このフェーズアーキテクチャを理解することで、特定のパターンがレイテンシを引き起こす理由を説明できます。 ```javascript // event-loop-phases.js // 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が、最も信頼性の高い検出メカニズムを提供します。 ```javascript // event-loop-monitor.js // 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つのパターンに起因します。 ```javascript // blocking-patterns.js // 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`モジュールは、単一のポートを共有するワーカープロセスを生成し、すべてのコアに受信接続を分散します。 ```javascript // cluster-setup.js // 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つのワーカーのクラッシュは他のワーカーに影響しません。 ## Worker Threadsを使用したCPU集約的タスクの処理 `cluster`モジュールはプロセス全体を複製します。Worker Threadsは、Node.js 10で導入されNode.js 12以降で安定版となったもので、同じプロセス内の並列スレッドでJavaScriptを実行します。`SharedArrayBuffer`を通じてメモリを共有し、構造化クローンを介してデータを転送します。 この区別は重要です。I/OバウンドのHTTPサーバーをコア全体にスケーリングするにはクラスターを使用し、イベントループからCPUバウンドの操作をオフロードするにはWorker Threadsを使用します。 ```javascript // worker-pool.js // 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 }; ``` ```javascript // image-worker.js // 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ポーズが可視的なレイテンシスパイクを引き起こすまで)に起因します。 ```javascript // memory-optimization.js // 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プロファイルにインラインキャッシュデータを含む改善されたプロファイリングサポートが含まれており、パフォーマンス分析がより正確になっています。 ```javascript // otel-setup.js // 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にアラートを設定することで、ユーザーが気づく前にパフォーマンス低下を検知できます。 > **クラスター + Worker Threadの注意点** > > 各クラスターワーカーは独自の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で実行する場合に価値があります。 > **Node.js 24のパフォーマンス向上** > > 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](https://opentelemetry.io/docs/languages/js/)とV8プロファイリングツールによる本番監視は、推測をデータに変えます。イベントループラグのp99とヒープ成長率にアラートを設定してください - Node.js 24はV8 12.4のパフォーマンス向上、Undici 7による安定したHTTP/3、成熟したパーミッションモデルを提供します。Node.js 20以前からのアップグレードで測定可能なスループット改善が得られます - 既存のNode.jsアプリケーションで`performance.eventLoopUtilization()`を使用することから始めてください。多くの場合、利用可能な最もインパクトの高い最適化が明らかになります --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/node-nestjs/nodejs-performance-event-loop-clustering-optimization