マイクロタスクキューとは何か(まずざっくりイメージ)
マイクロタスクキューは、
「とても優先度の高い“あとで実行する処理”が並ぶ、特別レーンの待ち行列」 です。
前回の「タスクキュー」は、setTimeout や DOM イベントなどのコールバックが並ぶ、普通の待ち行列でした。
マイクロタスクキューは、それよりも “先に必ず片付けたい用事” を入れておくための、別枠のキュー だと思ってください。
ここが重要です。
Promise の then や async/await の「await の先の処理」は、
普通のタスクキューではなく、この マイクロタスクキューに積まれ、次のタスクに進む前に必ず全部実行されます。
まずイベントループ全体の流れをもう一度ざっくり
大枠:イベントループがやっていること
JavaScript のイベントループは、ざっくり次のように動き続けています。
- コールスタックが空いていたら、タスクキューから 1 件取り出して実行する
- そのタスクが終わったら、
「次のタスクに行く前に、マイクロタスクキューの中身を全部処理する」 - それが終わったら、また 1 に戻る
つまり、
- 通常のタスク(setTimeout やイベントハンドラ)は「1 回に 1 つ」
- マイクロタスク(Promise.then など)は、「その都度、溜まっているものを全部」
というルールで実行されます。
ここが重要です。
「タスク → マイクロタスク全部 → 次のタスク → マイクロタスク全部 → …」
このサイクルが、イベントループの基本パターンです。
マイクロタスクに乗る代表選手は何か
いちばん大事なのは Promise 関連
マイクロタスクキューに積まれる代表的なものは次の通りです。
Promise の then, catch, finally のコールバックasync/await の「await の先の処理」(内部的には Promise.then 相当)queueMicrotask で登録された処理
逆に、setTimeout や DOM イベントのコールバックは 通常のタスクキュー に入ります。
この違いが、
「なぜ then の中の処理が、setTimeout(..., 0) より先に実行されるのか?」
という不思議を生みますが、マイクロタスクキューを知るときれいに説明できます。
具体例1:Promise と setTimeout の順番比較
コードと結果
次のコードを見てください。
console.log("A");
setTimeout(() => {
console.log("B: timeout");
}, 0);
Promise.resolve().then(() => {
console.log("C: promise");
});
console.log("D");
JavaScript実行結果は、おおよそこの順になります。
A
D
C: promise
B: timeout
「0ミリ秒の setTimeout より、Promise.then が先に出る」
ここにマイクロタスクキューの存在が効いています。
裏側で何が起きているか(丁寧に分解)
"A"を出力(同期)setTimeout(..., 0)を呼び、Web API に「すぐにこのコールバックをタスクキューに入れてね」と依頼
→ コールバックは 通常のタスクキュー 行き候補Promise.resolve().then(...)を呼ぶ
→ Promise はすぐに解決され、thenのコールバックが マイクロタスクキュー に追加される"D"を出力(同期)- グローバルコード(このスクリプトの同期部分)が終わり、コールスタックが空になる
- イベントループは、「次の通常タスクに行く前に、マイクロタスクキューを全部処理する」
→ マイクロタスクキューのC: promiseを実行 - マイクロタスクが終わったら、次に通常のタスクキューを見て、
B: timeoutを実行
この「マイクロタスクが通常タスクより先に全部処理される」ルールがあるので、C が B より先に出るのです。
ここが重要です。
Promise.then(マイクロタスク)は「今の処理が終わったら、次のタスクに行く前に必ずやること」として扱われる。
setTimeout(通常タスク)は「次のサイクルでやること」として後回しになる。
具体例2:マイクロタスクがたくさん積まれたらどうなるか
Promise をループでつなげる例
console.log("start");
Promise.resolve().then(function step1() {
console.log("step1");
return Promise.resolve();
}).then(function step2() {
console.log("step2");
}).then(function step3() {
console.log("step3");
});
console.log("end");
JavaScriptこのときの出力順は、
start
end
step1
step2
step3
のイメージになります。
Promise の then をチェーンすると、それぞれの then のコールバックは「マイクロタスク」として次々に積まれ、
「1つ実行 → その中で新しいマイクロタスクを登録 → 実行可能なマイクロタスクを全部処理」
…という流れで、一気に順番に実行されます。
同じことを setTimeout で書いたときとの違い
もし全部を setTimeout(..., 0) で書いたら、
タスクキューで 1 回ずつ後回しになるので、間に他の処理が割り込む余地が増えます。
マイクロタスクは「今やっているタスクが終わったら、一気に処理される」ので、
Promise チェーンは、同期コードとかなり近い感覚のまとまりとして実行されやすい、という特徴があります。
具体例3:マイクロタスクが UI 更新を遅らせるケース(少しだけ応用)
連続して大量のマイクロタスクを登録すると
マイクロタスクキューは「各タスクが終わるたびに全部処理し切る」という性質があるので、
Promise.then や queueMicrotask を使ってひたすらマイクロタスクを追加し続けると、
次の通常タスクや描画がなかなか来ない、という状態を作れてしまいます。
極端なイメージですが、例えば:
function spamMicrotask() {
queueMicrotask(() => {
spamMicrotask();
});
}
spamMicrotask();
JavaScriptのようなコードを書くと、イベントループが
「タスク1 → マイクロタスク無限 → …」という感じで、通常タスクに戻れなくなりかねません。
現実のコードでこんなことはあまりしませんが、
ここが重要です。
マイクロタスクは便利で優先度も高い一方、「次のタスクに行く前に全部処理される」というルールのせいで、使い方を誤ると他の処理を“詰まらせる”こともある
ということは頭の片隅に置いておくといいです。
なぜ「Promise はマイクロタスク」である必要があるのか
一貫した順序と予測可能性のため
Promise / async/await は、「非同期だけど同期っぽく書ける」ことが売りです。
例えば、
console.log("A");
Promise.resolve().then(() => {
console.log("B");
});
console.log("C");
JavaScriptこれで
A
C
B
という順になるのは、
「Promise の then は、必ず同期処理が全部終わったあとに実行される」
という保証があるからです。
もしこれが通常タスク扱いだと、
- 他の
setTimeoutやイベントの影響で順番が前後する - 同じイベントループのサイクル内で実行されない可能性が出る
など、挙動が不安定になります。
マイクロタスクとして扱うことで、
- 「今のタスクが終わった瞬間」に
- 「他の通常タスクより先に」
- 「キューに積まれた順に全部」
実行されるので、
Promise ベースの非同期処理はとても予測しやすくなります。
async/await の「読みやすさ」とも直結している
async function main() {
console.log("1");
await Promise.resolve();
console.log("2");
}
main();
console.log("3");
JavaScriptこれが
1
3
2
という順になるのは、
awaitの行でいったん関数を抜ける- Promise が解決されたときに、「続き」がマイクロタスクとしてスケジュールされる
- 同期コード(”3″)がすべて終わったあと、マイクロタスクとして “2” が実行される
という流れのおかげです。
async/await を「同期っぽく見える非同期」として安心して使えるのは、
裏でマイクロタスクキューがきちんと約束を守ってくれているからです。
初心者として「どこまで理解しておくといいか」
最低限押さえておきたいポイント
マイクロタスクキューについて、今の段階でしっかり持っておいてほしいのは次の感覚です。
1つめに、JavaScript には「通常のタスクキュー」と「マイクロタスクキュー」という2種類の待ち行列がある。
2つめに、setTimeout やイベントハンドラは通常タスクキューに、Promise.then や async/await の続きはマイクロタスクキューに積まれる。
みっつめに、イベントループは
「タスクを 1 つ処理するたびに、次のタスクに行く前にマイクロタスクキューを全部処理する」
という優先ルールで動いている。
その結果として、Promise.then の中の処理は setTimeout(..., 0) より早く実行されることが多い。
余裕が出てきたら試してほしいこと
小さな実験として、
console.log,setTimeout,Promise.resolve().thenを混ぜたコードを書いて、実行順を予想してから実際に見る- then の中でさらに then を返してみて、「マイクロタスクが連鎖していく」感じを味わう
queueMicrotaskを1回だけ使って、setTimeout(..., 0)と順番比較してみる
などをしてみると、
マイクロタスクキューが「ただの概念」ではなく、「挙動の裏側でちゃんと動いている仕組み」として掴めてきます。
まとめ
マイクロタスクキューの本質は、
「Promise などの“優先的に片付けたい非同期処理”を、次のタスクに進む前にまとめて処理するための特別な待ち行列」
です。
押さえておきたい要点をもう一度整理すると、
マイクロタスクキューには、主に Promise.then, catch, finally や async/await の続き、queueMicrotask で登録した処理が入る。
イベントループは、「通常のタスクを 1 つ実行 → マイクロタスクキューを全部処理 → 次のタスクへ…」という順で動いている。
そのため、Promise.then の処理は、setTimeout(..., 0) よりも先に実行されることが多い。
マイクロタスクを使いすぎると、通常タスクや描画が後回しになり、逆に詰まりの原因にもなりうる。
まずは、
「Promise の then や async/await の続きは、“すぐ後で必ず実行されるマイクロタスク”としてキューに積まれる」
というイメージをしっかり握っておいてください。
この「タスクキュー」と「マイクロタスクキュー」の違いが腑に落ちると、
JavaScript の非同期コードの実行順は、ほとんどのケースで「なるほどそういうことか」と説明できるようになります。
