Node.jsパフォーマンス最適化:イベントループ、クラスタリング、最適化テクニック2026年版

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

Node.jsパフォーマンス最適化:イベントループとクラスタリングの解説図

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つの異なるフェーズで動作します。各フェーズは特定のカテゴリのコールバックを担当しており、このフェーズアーキテクチャを理解することで、特定のパターンがレイテンシを引き起こす理由を説明できます。

event-loop-phases.jsjavascript
// 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が、最も信頼性の高い検出メカニズムを提供します。

event-loop-monitor.jsjavascript
// 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つのパターンに起因します。

blocking-patterns.jsjavascript
// 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モジュールは、単一のポートを共有するワーカープロセスを生成し、すべてのコアに受信接続を分散します。

cluster-setup.jsjavascript
// 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を使用します。

worker-pool.jsjavascript
// 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 };
image-worker.jsjavascript
// 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ポーズが可視的なレイテンシスパイクを引き起こすまで)に起因します。

memory-optimization.jsjavascript
// 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プロファイルにインラインキャッシュデータを含む改善されたプロファイリングサポートが含まれており、パフォーマンス分析がより正確になっています。

otel-setup.jsjavascript
// 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が多いアプリには少なすぎ、クラスター構成では多すぎる
  • 大きなデータにはストリーミングを使用JSONStreamcsv-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()を使用することから始めてください。多くの場合、利用可能な最もインパクトの高い最適化が明らかになります

タグ

#node.js
#performance
#event-loop
#clustering
#optimization
#worker-threads

共有

関連記事