Node.jsイベントループの深淵:`process.nextTick`と`Promise`の優先順位を完全掌握する
こんにちは。開発環境の深淵へようこそ。
Node.jsのコードを書いていると、「なぜか意図した順番でログが出力されない」「Promiseの解決が後回しにされている気がする」といった現象に遭遇しませんか?実はそれ、Node.jsの心臓部である「イベントループ」の優先順位ルールを理解することで、全てが論理的に説明できるようになります。
今日は、表面的なチュートリアルを超えて、Node.jsを使いこなすための「実行順序の哲学」を伝授します。
—
1. Node.jsのイベントループ:その「脳内構造」を知る
Node.jsはシングルスレッドです。この制約の中で、I/O処理やタイマーといった「重い処理」を効率的に捌くために、いくつかの「待機列(キュー)」を使い分けています。
重要なのは、「どのキューが、どのタイミングで処理されるか」という優先順位です。
優先順位の序列(実行のルール)
1. スタック(メインの同期コード):真っ先に実行されます。
2. `process.nextTick` キュー:イベントループのフェーズに関係なく、現在の処理が完了した直後に割り込み実行されます。
3. マイクロタスクキュー(Promise等):`nextTick` の直後に実行されます。
4. マクロタスクキュー(`setTimeout`, `setImmediate` 等):イベントループの各フェーズで順番に処理されます。
2. 実践:なぜ `nextTick` が最強なのか?
まずは、以下のコードを動かして、ブラウザのJavaScriptエンジンとは少し異なるNode.js独自の挙動を体感してみましょう。
// event-loop.js
console.log(‘1: 同期処理の開始’);
setTimeout(() => console.log(‘2: setTimeout (マクロタスク)’), 0);
Promise.resolve().then(() => console.log(‘3: Promise (マイクロタスク)’));
process.nextTick(() => console.log(‘4: process.nextTick (割り込みタスク)’));
console.log(‘5: 同期処理の終了’);
実行結果のログ
$ node event-loop.js
1: 同期処理の開始
5: 同期処理の終了
4: process.nextTick (割り込みタスク)
3: Promise (マイクロタスク)
2: setTimeout (マクロタスク)
なぜこの順序になるのか?
- 同期コードが優先: まず `1` と `5` が表示されます。
- `nextTick` の特権: これは「マイクロタスク」よりも優先されます。Node.js内部では「現在の処理が完了したら、他のタスクに渡す前にこれだけは実行してくれ」という命令として扱われるからです。
- Promiseの次: マイクロタスクであるPromiseが処理されます。
- 最後にマクロタスク: `setTimeout` はイベントループのタイマーフェーズまで待機するため、最後になります。
—
3. `setImmediate` と `setTimeout` の微妙な関係
皆さんが現場で混乱しやすいのが `setImmediate` です。これは「次のイベントループのチェックフェーズで実行せよ」という意味です。
setTimeout(() => console.log(‘setTimeout’), 0);
setImmediate(() => console.log(‘setImmediate’));
この結果は実は「実行するたびに変わる可能性」があります。なぜなら、Node.jsの起動直後のタイマー精度やCPU負荷によって、イベントループがどちらのフェーズに先に到達するかが不安定だからです。
設計者の教訓: 「どちらが先か」に依存するコードはバグの温床です。制御フローは常に明示的であるべきです。
—
4. イベントループの「ブロッキング」を回避する思考法
Node.jsのパフォーマンスを低下させる最大の要因は、イベントループを止めることです。
悪いコード(イベントループの停止)
// 重い計算処理
while (true) {
// ここでCPUを使い切り、他のタイマーやI/Oが一切処理されなくなる
}
この状況に陥ると、サーバーは無反応になります。これを解決するアーキテクト的なアプローチは以下の通りです。
1. 重い処理を非同期に分割する: `setImmediate()` を使って、処理の合間にイベントループに「息継ぎ(他のタスクの処理)」をさせる。
2. Worker Threadsの活用: 計算負荷が高い処理は `worker_threads` モジュールでメインスレッドから切り離す。
3. ストリーミング処理: メモリを食いつぶさないよう、`fs.createReadStream` などでデータを小分けに処理する。
—
まとめ:あなたのコードを洗練させるために
Node.jsのイベントループを理解するということは、「コードがいつ実行されるか」をコントロールする権利を持つということです。
- 即座に実行したい優先事項には `process.nextTick`。
- 非同期処理の結果には `Promise`。
- イベントループの合間に差し込みたい処理には `setImmediate`。
これらを適切に使い分けることで、あなたの書くコードは驚くほど堅牢で、予測可能な挙動をするようになります。まずは、上記の `event-loop.js` を実行し、自分なりのタスクを追加して優先順位を実験してみてください。
理論と実践が結びついたとき、あなたはもう「Node.jsを書く人」から「Node.jsを設計する人」へと進化しています。次回の開発現場では、この優先順位を意識してコードを組んでみてください。きっと、デバッグの時間が劇的に短縮されるはずですよ。