「なんでこの順で動くの?」を一発で理解する:JS/TSのイベントループと実行順(同期→Microtask→描画→Macrotask)
非同期処理のコードは書けるけど、実行順に自信がない。
await や setTimeout、Promise.then が どの順で動くかが直感とズレる。
この記事は、そんな人のために イベントループと「tick」 を軸に、実行順のルールをスッキリ言語化します。
この記事のゴール
- イベントループの流れを、同期→Microtask→描画→Macrotask の順で理解する
-
await(= 非同期の続きがいつ動く?)/Promise.then/setTimeoutの 実行順が説明できるようになる
用語の解説
-
Call Stack(コールスタック)
「今、実行中の関数の山」。同期コードはここで一気に走る。 -
Queues(キュー)
「後で実行する処理」の待ち行列。-
Microtask Queue:
Promise.then/catch/finally、queueMicrotask、awaitの“続き” -
Macrotask (Task) Queue:
setTimeout,setInterval,setImmediate(Node)など
-
Microtask Queue:
-
描画(ブラウザ)
レイアウト/ペイント。Microtask を処理した後に行われる。 -
tick(ティック)
イベントループ1周のこと。
1 tick の中で「同期→Microtask全消化→(必要なら)描画→Macrotaskを1つ実行」までが行われ、次の tick に進む。
イベントループの基本ルール
- tick(ティック) … イベントループの 1 周 の単位
-
Macrotask を 1 つ取り出して実行
この“中身”として 同期コードが Call Stack で走る。 -
Microtask Queue を空になるまで全部実行
Promise.then/catch/finally、await の続き、queueMicrotask など。 - (ブラウザ)必要なら描画(レイアウト/ペイント)
- 次の tick へ(次の Task/Macrotask を 1 つ実行)
console.log("A"); // ← Tick N の macrotask(同期)
setTimeout(() => console.log("timeout (macro)"), 0); // ← 次の macrotask(= Tick N+1)
Promise.resolve()
.then(() => console.log("microtask 1")) // ← Tick N の“末尾”で実行
.then(() => console.log("microtask 2"));
console.log("B"); // ← Tick N の macrotask(同期)
最短で実行順を体感する例
console.log("A");
setTimeout(() => console.log("timeout (macro)"), 0);
Promise.resolve()
.then(() => console.log("microtask 1"))
.then(() => console.log("microtask 2"));
console.log("B");
出力:A → B → microtask 1 → microtask 2 → timeout (macro)
-
A,Bは同期なので即実行(Call Stack) -
同じ tick の末尾で Microtask を全部実行(
then-1→then-2) - その次の tickで Macrotask(
setTimeout)を実行
await はいつ動く?——「関数の続き」が Microtask で再開
async function main() {
console.log("A");
const res = await fetch("/api/user"); // ← ここで「この関数の続き」を一時停止
console.log("B"); // ← 解決後、Microtask として再開
}
main();
console.log("C");
出力:A → C → (fetch完了後)B
-
awaitは Promise が解決するまで「その関数の続き」だけ一時停止 - 解決したら 続きが Microtask としてキューに入る → 今の tick が終わったタイミングで再開
- スレッド全体は止まらない(UIは固まらない)
awaitは 非同期を同期“っぽく”書ける糖衣構文。裏側はイベントループ+Microtask が仕事しているだけ。
「Microtask は同期の一番最後?」「setTimeout は次の tick?」に答える
-
Microtask:今の tick の末尾で、空になるまで全部実行される
→ 体感的には「同期の最後」に動く -
setTimeout(..., 0)のコールバック(Macrotask):
次の tick で実行される(Microtask より後)
つまり、「今すぐこの tick の最後に突っ込みたい」→ Microtask
「次のタイミング(次の tick)でいい」→ Macrotask
「Call Stack と Microtask が並列で走る瞬間は?」→ ない
- JS の実行は基本 1スレッド
-
同時実行はしない。
同期(Call Stack)を終える → Microtask を全部処理 → Macrotask …と順番に切り替えるだけ。
Node.js には I/O 用のスレッドプールがあるが、JS本体の実行は常に1スレッド。
2つのキューの動作をまとめて確認
console.log("A");
setTimeout(() => console.log("[macro] timeout"), 0); // 次の tick
Promise.resolve()
.then(() => console.log("[micro] then-1"))
.then(() => console.log("[micro] then-2")); // 同じ tick の末尾
queueMicrotask(() => console.log("[micro] qMicro")); // 同じ tick の末尾
console.log("B");
/*
A
B
[micro] then-1
[micro] then-2
[micro] qMicro
[macro] timeout
*/
-
同期:
A,B -
Microtask(今の tick の末尾で全消化):
then-1→then-2→qMicro -
Macrotask(次の tick):
timeout
実務チートシート
-
今の tick の末尾にすぐ入れたい
→queueMicrotask(fn)/Promise.resolve().then(fn)(Microtask) -
次のタイミング(次の tick)でよい
→setTimeout(fn, 0)/setImmediate(Node)(Macrotask) -
描画直前や毎フレームで実行したい(ブラウザ)
→requestAnimationFrame(fn)
まとめ
- 1 tick= 同期 → Microtask(全消化)→ 描画 → Macrotask(1つ)
- Microtask は 今の tick の末尾でまとめて動く
-
setTimeoutは 次の tick - 並列実行はしない(JS本体は1スレッド)。順番に切り替えるだけ
実行順のルールが腹落ちすると、非同期コードの “なぜこの順?” が消えて、デバッグも設計も一段ラクになります。ここを土台に、
Promise.allなどの描画最適化なども攻めていきましょう。
Discussion
WebWorker 等の Worker の利用が別途必要ですね。(Promise 返す様にして触りやすい関数作ったりしますが)
その為、 thread 間でオブジェクトが共有されるとまずいので structuredClone で コピーして送信する。
コピーコストが高い ArrayBuffer 等は structuredClone の第二引数により 移譲して そのまま送信したりしてコスト低減を試みる