Node.js worker_threadsでCPU集約タスクをメインスレッドから分離する

問題

Node.jsで画像リサイズや大量のJSONパースなどのCPU集約的な処理を実行すると、イベントループがブロックされます。APIレスポンスが停止し、他のリクエストも処理できなくなります。

解決方法

worker_threadsモジュールを使って、重い処理を別スレッドで実行します。

// worker.js - ワーカースレッドで実行されるコード
const { parentPort, workerData } = require('worker_threads');

function heavyCalculation(data) {
  let result = 0;
  for (let i = 0; i < data.iterations; i++) {
    result += Math.sqrt(i) * Math.random();
  }
  return result;
}

const result = heavyCalculation(workerData);
parentPort.postMessage(result);
// main.js - メインスレッド
const { Worker } = require('worker_threads');

function runWorker(data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js', { workerData: data });
    worker.on('message', resolve);
    worker.on('error', reject);
  });
}

app.get('/heavy', async (req, res) => {
  const result = await runWorker({ iterations: 10_000_000 });
  res.json({ result });
  // この間、他のリクエストは正常に処理されます
});

繰り返し使う場合は、ワーカープールでオーバーヘッドを削減できます。

const os = require('os');
const poolSize = os.cpus().length;
const workers = Array.from({ length: poolSize },
  () => new Worker('./worker.js')
);

ポイント

  • worker_threadsはNode.js組み込みモジュールなので、追加インストール不要です
  • workerDataでデータを渡し、parentPort.postMessage()で結果を返します
  • メインスレッドのイベントループをブロックしないため、APIの応答性が維持されます
  • 繰り返し使用する場合はワーカープールパターンで生成オーバーヘッドを削減しましょう