- JavaScript
JavaScriptイベントループ|マイクロタスクの実行順を図とクイズで
イベントループとは、JavaScriptが「いま実行中の処理」を終えたあと、次に何を実行するかを決めるしくみです。 ルールの中心は1つで、「タスクを1つ実行 → マイクロタスクを全部実行 →(ブラウザなら)必要に応じて描画 → 次のタスク」 をくり返します。setTimeout はタスク、Promise の then や await の続きはマイクロタスクなので、setTimeout(…, 0) より Promise.resolve().then(…) が先に動きます。
この記事では、コールスタック・タスクキュー・マイクロタスクキュー・描画の関係をMermaidの図で示し、同期処理・process.nextTick・Promise・queueMicrotask・await・setTimeout・setImmediate・I/O・requestAnimationFrame・MessageChannel の優先順位の一覧表にまとめます。さらに、async/await を含む実行順クイズ10問を出題し、Node.js 22(v22.22.2、CommonJSとESモジュールの両方)とChromium 141(Playwright)での実測ログで答え合わせします。環境で順番が変わる問題はその理由も説明します。後半では、長い同期処理で画面が止まる様子と、setTimeout・scheduler.yield()・MessageChannel で分割したときの描画回数を実測します。async/await の書き方や Promise.all の使い分けは別記事で扱っているため、ここでは「実行の順番」に絞ります。
イベントループの早見表(実行の優先順位)
上から順に実行されます。同じ段の中は登録した順です。
| 順位 | 種類 | 代表的なAPI | 環境 | ポイント |
|---|---|---|---|---|
| 1 | 同期処理(コールスタック) | 普通の関数呼び出し、new Promise の中の関数、async 関数の最初の await まで |
共通 | 最後まで止まらずに走る |
| 2 | nextTickキュー | process.nextTick(fn) |
Node.js | CommonJSではPromiseより先。ESモジュールのトップレベルでは後(クイズ8) |
| 3 | マイクロタスク | Promise.then/catch/finally、queueMicrotask(fn)、await の続き、MutationObserver |
共通 | キューが空になるまで全部実行。途中で追加された分も続けて実行 |
| 4 | 描画の準備 | requestAnimationFrame(fn) |
ブラウザ | 描画するフレームの直前だけ(だいたい16.7msごと) |
| 5 | 描画 | スタイル計算・レイアウト・ペイント | ブラウザ | タスクとタスクの間でしか起きない |
| 6 | タスク(マクロタスク) | setTimeout、setInterval、MessageChannel、DOMイベント、fetch の完了 |
共通 | 1回のループで1つだけ取り出す |
| 6' | Nodeのフェーズ | timers(setTimeout)→ poll(I/O)→ check(setImmediate)→ close |
Node.js | I/Oのコールバック内では setImmediate が setTimeout(0) より先(クイズ9) |
覚え方は「同期 → nextTick → マイクロタスク全部 → (描画)→ タスク1つ → マイクロタスク全部 → …」です。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JavaScriptイベントループ|マイクロタスクの実行順を図とクイズで」
イベントループの全体図
JavaScriptのエンジン(V8など)は、実行中の関数を積む「コールスタック」を1本だけ持っています。 タイマーやネットワークの待ち時間は、ブラウザやNode.js(ホスト環境)が受け持ち、終わったらコールバックをキューに入れます。イベントループは、コールスタックが空になるたびにキューから次の仕事を取り出します。
flowchart TD
A[タスクキューから1つ取り出して実行<br/>setTimeout / イベント / MessageChannel] --> B{コールスタックが空?}
B -- はい --> C[nextTickキューを全部実行<br/>Node.jsのみ]
C --> D[マイクロタスクキューを全部実行<br/>Promise.then / queueMicrotask / awaitの続き]
D --> E{実行中に新しい<br/>マイクロタスクが入った?}
E -- はい --> D
E -- いいえ --> F{描画のタイミング?<br/>ブラウザ・約16.7msごと}
F -- はい --> G[requestAnimationFrame を実行]
G --> H[スタイル計算 → レイアウト → ペイント]
H --> A
F -- いいえ --> A
Node.jsでは、タスクの取り出しが「フェーズ」に分かれています。
flowchart LR
T[timers<br/>setTimeout / setInterval] --> P[pending callbacks]
P --> PO[poll<br/>I/Oのコールバック]
PO --> CH[check<br/>setImmediate]
CH --> CL[close callbacks]
CL --> T
Node.jsでも、各コールバックが終わるたびに「nextTickキュー → マイクロタスクキュー」が空になるまで実行されます(Node 11以降。ブラウザと同じ動き)。
用語の早見表
| 用語 | 意味 |
|---|---|
| コールスタック | 実行中の関数が積まれる場所。空になるまで次の仕事に移らない |
| タスク(マクロタスク) | イベントループが1回に1つ取り出す仕事。setTimeout のコールバックなど |
| マイクロタスク | 今のタスクの直後、描画より前にまとめて実行される小さな仕事 |
| タスクソース | タスクの種類(タイマー、ユーザー操作、メッセージなど)。種類が違うと順番は保証されない |
| レンダリング | 画面の描き直し。タスクの合間にだけ行われる |
実行順クイズ10問(答えはこのあと)
まず自分で順番を予想してから、次の章の実測ログで確かめてみてください。 出力は console.log の順番を答えます。Q8・Q9はNode.js専用、Q10はブラウザとNodeで違いが出る問題です。
Q1. 基本の4行
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
Q2. awaitの前後
async function f() {
console.log("A");
await null;
console.log("B");
}
console.log("1");
f();
console.log("2");
Q3. タイマーの中のPromise、Promiseの中のタイマー
setTimeout(() => {
console.log("t1");
Promise.resolve().then(() => console.log("p-in-t1"));
}, 0);
setTimeout(() => console.log("t2"), 0);
Promise.resolve().then(() => {
console.log("p1");
setTimeout(() => console.log("t-in-p1"), 0);
});
Q4. 2本のthenチェーンとqueueMicrotask
Promise.resolve().then(() => console.log("a1")).then(() => console.log("a2"));
Promise.resolve().then(() => console.log("b1")).then(() => console.log("b2"));
queueMicrotask(() => console.log("q"));
Q5. async関数の呼び出しとnew Promise
async function a() {
console.log("a-start");
await b();
console.log("a-end");
}
async function b() { console.log("b"); }
setTimeout(() => console.log("timeout"), 0);
a();
new Promise((resolve) => { console.log("executor"); resolve(); })
.then(() => console.log("then"));
console.log("sync-end");
Q6. async関数がPromiseをreturnする
async function g() { return Promise.resolve("g"); }
g().then((v) => console.log(v));
Promise.resolve()
.then(() => console.log("x1"))
.then(() => console.log("x2"))
.then(() => console.log("x3"));
Q7. awaitを2回とthenチェーン
async function h() {
console.log("h1");
await undefined;
console.log("h2");
await undefined;
console.log("h3");
}
h();
Promise.resolve().then(() => console.log("p1")).then(() => console.log("p2"));
Q8. process.nextTickとPromise(Node.js)
Promise.resolve().then(() => console.log("promise"));
process.nextTick(() => console.log("nextTick"));
queueMicrotask(() => console.log("queueMicrotask"));
console.log("sync");
Q9. setTimeoutとsetImmediate(Node.js、CommonJS)
const { readFile } = require("node:fs");
setTimeout(() => console.log("main: timeout"), 0);
setImmediate(() => console.log("main: immediate"));
readFile(__filename, () => {
setTimeout(() => console.log("io: timeout"), 0);
setImmediate(() => console.log("io: immediate"));
});
Q10. MessageChannel・setTimeout・requestAnimationFrame
const ch = new MessageChannel();
ch.port1.onmessage = () => { console.log("message"); ch.port1.close(); };
setTimeout(() => console.log("timeout"), 0);
if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => console.log("rAF"));
ch.port2.postMessage(null);
Promise.resolve().then(() => console.log("promise"));
console.log("sync");
答え合わせ:Node.js 22とChromium 141の実測ログ
10問を、Node.js v22.22.2(.cjs と .mjs の両方)と、Playwright経由のChromium 141.0.7390.37(<script> タグとして実行、3回)で動かしました。 出力を1行にまとめています。
| 問 | Node.js 22(CommonJS) | Node.js 22(ESモジュール) | Chromium 141 | 違い |
|---|---|---|---|---|
| Q1 | 1 4 3 2 |
1 4 3 2 |
1 4 3 2 |
なし |
| Q2 | 1 A 2 B |
1 A 2 B |
1 A 2 B |
なし |
| Q3 | p1 t1 p-in-t1 t2 t-in-p1 |
同じ | 同じ | なし |
| Q4 | a1 b1 q a2 b2 |
同じ | 同じ | なし |
| Q5 | a-start b executor sync-end a-end then timeout |
同じ | 同じ | なし |
| Q6 | x1 x2 g x3 |
同じ | 同じ | なし |
| Q7 | h1 h2 p1 h3 p2 |
同じ | 同じ | なし |
| Q8 | sync nextTick promise queueMicrotask |
sync promise queueMicrotask nextTick |
ReferenceError: process is not defined(そのあと promise) |
CJSとESMで逆転 |
| Q9 | 30回中22回 main: timeout → main: immediate、8回は逆。I/O内は30回とも io: immediate → io: timeout |
(CJS専用) | 対象外 | mainの順番は毎回変わる |
| Q10 | sync promise timeout message(10回とも) |
同じ | sync promise timeout message rAF(3回とも) |
rAFはブラウザのみ |
答え合わせに使ったスクリプトです。クイズのコードを1つのファイルにまとめ、Nodeは別プロセスで、ブラウザはページに <script> として入れて実行しています。
// run_node.mjs (quizzes.mjs に { Q1: "コード", ... } を書いておく)
import { quizzes } from "./quizzes.mjs";
import { writeFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
for (const [id, code] of Object.entries(quizzes)) {
const runs = [];
for (const ext of ["cjs", "mjs"]) {
if (ext === "mjs" && id === "Q9") continue; // Q9はrequireを使うのでCJSのみ
const f = `/tmp/quiz_${id}.${ext}`; writeFileSync(f, code);
runs.push(`${ext}: ` + execFileSync("node", [f]).toString().trim().split("\n").join(" "));
}
console.log(id, "|", runs.join(" || "));
}
// run_browser.mjs (npm i playwright。インストール済みのChromiumを使用)
import { chromium } from "playwright";
import { quizzes } from "./quizzes.mjs";
const browser = await chromium.launch();
console.log("Chromium", browser.version());
for (const [id, code] of Object.entries(quizzes)) {
const page = await browser.newPage();
await page.setContent("<!doctype html><title>q</title><body>q</body>");
const logs = [];
page.on("console", (m) => logs.push(m.text()));
page.on("pageerror", (e) => logs.push("ERROR: " + e.message));
await page.addScriptTag({ content: code });
await page.waitForTimeout(300);
console.log(id, "|", logs.join(" "));
await page.close();
}
await browser.close();
Node.jsでの実行結果:
Q1 | cjs: 1 4 3 2 || mjs: 1 4 3 2
Q2 | cjs: 1 A 2 B || mjs: 1 A 2 B
Q3 | cjs: p1 t1 p-in-t1 t2 t-in-p1 || mjs: p1 t1 p-in-t1 t2 t-in-p1
Q4 | cjs: a1 b1 q a2 b2 || mjs: a1 b1 q a2 b2
Q5 | cjs: a-start b executor sync-end a-end then timeout || mjs: a-start b executor sync-end a-end then timeout
Q6 | cjs: x1 x2 g x3 || mjs: x1 x2 g x3
Q7 | cjs: h1 h2 p1 h3 p2 || mjs: h1 h2 p1 h3 p2
Q8 | cjs: sync nextTick promise queueMicrotask || mjs: sync promise queueMicrotask nextTick
Q9 | cjs: main: immediate main: timeout io: immediate io: timeout
Q10 | cjs: sync promise timeout message || mjs: sync promise timeout message
Chromiumでの実行結果(3回とも同じ。1回分を抜粋):
Chromium 141.0.7390.37
Q1 | 1 4 3 2
Q2 | 1 A 2 B
Q3 | p1 t1 p-in-t1 t2 t-in-p1
Q4 | a1 b1 q a2 b2
Q5 | a-start b executor sync-end a-end then timeout
Q6 | x1 x2 g x3
Q7 | h1 h2 p1 h3 p2
Q8 | ERROR: process is not defined promise
Q9 | ERROR: require is not defined
Q10 | sync promise timeout message rAF
各問の解説
Q1(1 4 3 2):同期 → マイクロタスク → タスク。 1 と 4 は同期処理。then のコールバックはマイクロタスク、setTimeout はタスクなので、3 が 2 より先です。遅延を 0 にしても「次のタスク」になるだけで、すぐには動きません。
Q2(1 A 2 B):await までは同期で動く。 async 関数は呼んだ瞬間に最初の await まで同期で走るので A は 2 より先です。await の続き(B)はマイクロタスクとして予約されます。
Q3(p1 t1 p-in-t1 t2 t-in-p1):タスク1つごとにマイクロタスクが全部実行される。 t1 のタスクが終わった時点で、そこで登録した p-in-t1 が t2 より先に動きます。t-in-p1 は p1 の中で登録されたタイマーなので、t1・t2 より後ろに並びます。
Q4(a1 b1 q a2 b2):チェーンの次の then は「前の then が終わってから」キューに入る。 最初の時点でキューにあるのは a1・b1・q の3つです。a1 が終わって初めて a2 が入るため、q が a2 より先になります。
Q5(a-start b executor sync-end a-end then timeout):new Promise の中の関数は同期。 b() の中身、new Promise に渡した関数(executor)はどちらも同期で実行されます。a-end(await b() の続き)と then は登録された順にマイクロタスクとして動き、timeout が最後です。
Q6(x1 x2 g x3):async 関数でPromiseを return すると、2回分遅れる。 return Promise.resolve(...) は、戻り値のPromiseを「中身を取り出してから解決する」ための追加のマイクロタスクが2つ入ります。値をそのまま返す return "g" なら g x1 x2 x3 の順になります。順番に依存したコードを書かないことが一番の対策です。
Q7(h1 h2 p1 h3 p2):await 1回につきマイクロタスク1回ぶん。 h2 と p1 は同じ「1巡目」、h3 と p2 が「2巡目」です。await は別のタスクに分かれるわけではなく、マイクロタスクの順番待ちに並ぶだけです。
Q8:CommonJSでは nextTick が先、ESモジュールでは後。 Node.jsは「nextTickキュー → マイクロタスクキュー」の順に処理します。ただしESモジュールはモジュールの評価そのものがPromiseのしくみの中で動くため、トップレベルの処理が終わった時点でまずマイクロタスクが実行され、nextTickはそのあとになります。Node.jsのドキュメントでも、多くの場合は queueMicrotask を使うことがすすめられています。
Q9:メインモジュールでの setTimeout(0) と setImmediate の順番は決まらない。 setTimeout(0) は実際には1ms以上の遅延になり、ループに入った時点で1msが経過しているかどうかで結果が変わります(実測では30回中22回が timeout 先)。I/Oのコールバックの中では、次はcheckフェーズなので必ず setImmediate が先です。「すぐ次に」実行したいなら setImmediate を選ぶと順番が安定します。
Q10:タスクの種類が違うと、順番は仕様で決まっていない。 setTimeout(タイマー)と MessageChannel(メッセージ)は別のタスクソースで、どちらを先に取り出すかはブラウザの実装に任されています。今回はNode・Chromiumとも timeout が先でした。requestAnimationFrame は描画の直前に動くため、すべてのタスクより後でした(描画のタイミングによって前後します)。
長い同期処理で画面が止まる例と、分割の実測
JavaScriptが1つのタスクを実行している間、ブラウザは描画もクリックの処理もできません。 そこで「10msの重い処理を100回(合計1秒)」を5通りの書き方で実行し、requestAnimationFrame で描画回数と最長の描画間隔を測りました。
// split.mjs (Playwright + Chromium 141。ページ内で実行する部分)
function work(ms) { const end = performance.now() + ms; while (performance.now() < end) {} }
async function measure(label, job) {
const frames = [];
let on = true;
const tick = (t) => { frames.push(t); document.getElementById("c").textContent = frames.length; if (on) requestAnimationFrame(tick); };
requestAnimationFrame(tick);
await new Promise((r) => setTimeout(r, 100));
const start = performance.now();
await job();
const total = performance.now() - start;
await new Promise((r) => setTimeout(r, 100));
on = false;
const inside = frames.filter((t) => t > start && t < start + total);
let maxGap = 0;
const w = frames.filter((t) => t > start - 20 && t < start + total + 40);
for (let i = 1; i < w.length; i++) maxGap = Math.max(maxGap, w[i] - w[i - 1]);
return `${label} 合計 ${total.toFixed(0)} ms / 処理中の描画 ${inside.length} 回 / 最長の描画間隔 ${maxGap.toFixed(0)} ms`;
}
const CHUNKS = 100, PER = 10; // 10ms × 100回 = 1秒分の処理
const results = [];
results.push(await measure("A. 一気に実行(同期)", async () => { work(CHUNKS * PER); }));
results.push(await measure("B. await null で区切る", async () => { for (let i = 0; i < CHUNKS; i++) { work(PER); await null; } }));
results.push(await measure("C. setTimeout(0) で区切る", async () => { for (let i = 0; i < CHUNKS; i++) { work(PER); await new Promise((r) => setTimeout(r, 0)); } }));
results.push(await measure("D. scheduler.yield() で区切る", async () => { for (let i = 0; i < CHUNKS; i++) { work(PER); await scheduler.yield(); } }));
results.push(await measure("E. MessageChannel で区切る", async () => { const ch = new MessageChannel(); for (let i = 0; i < CHUNKS; i++) { work(PER); await new Promise((r) => { ch.port1.onmessage = r; ch.port2.postMessage(0); }); } }));
実行結果(Chromium 141.0.7390.37、ヘッドレス):
Chromium 141.0.7390.37 scheduler.yield: function
A. 一気に実行(同期) 合計 1000 ms / 処理中の描画 2 回 / 最長の描画間隔 983 ms
B. await null で区切る 合計 1008 ms / 処理中の描画 2 回 / 最長の描画間隔 1000 ms
C. setTimeout(0) で区切る 合計 1476 ms / 処理中の描画 88 回 / 最長の描画間隔 17 ms
D. scheduler.yield() で区切る 合計 1032 ms / 処理中の描画 11 回 / 最長の描画間隔 117 ms
E. MessageChannel で区切る 合計 1043 ms / 処理中の描画 63 回 / 最長の描画間隔 17 ms
| 書き方 | 合計時間 | 画面の更新 | 向いている場面 |
|---|---|---|---|
| A. 一気に実行 | 最短 | 約1秒止まる | 50ms未満で終わる処理 |
B. await null で区切る |
Aとほぼ同じ | 止まる(マイクロタスクなので描画が入らない) | 区切りの効果はない |
C. setTimeout(0) |
約1.5倍 | なめらか(17ms間隔) | どこでも動く。合計時間は延びる |
D. scheduler.yield() |
ほぼ増えない | 描画は約100msおき(今回の計測) | 処理を早く終えつつ、入力にも応える |
E. MessageChannel |
ほぼ増えない | なめらか | scheduler.yield がない環境の代わり |
読み取れることは3つです。
awaitで区切っても画面は止まったままです(B)。awaitの続きはマイクロタスクで、マイクロタスクが全部終わるまで描画は起きません。「awaitを挟めば画面が動く」は誤解で、タスクに戻す(C〜E)必要があります。setTimeout(0)は合計時間が延びます(C)。 ブラウザはsetTimeoutが5段以上入れ子になると最小4msの待ちを入れるため、100回の区切りで約470msが加わりました。scheduler.yield()は「続きを優先して再開する」ため、合計時間がほとんど延びません(D)。 その代わり今回の計測では描画は約100msおきでした。ブラウザは続きの処理を優先しつつ、入力イベントや一定間隔での描画は差し込みます。アニメーションを毎フレーム更新したい処理ならC・E、処理を早く終えたいならDが向いています。
scheduler.yieldの対応状況と書き方
| 環境 | 対応 | 確認方法 |
|---|---|---|
| Chromium 141 | scheduler.yield が function(実測) |
typeof scheduler?.yield |
| Node.js 22 | グローバルの scheduler は undefined(実測)。node:timers/promises に実験的な scheduler.yield() があり、中身は setImmediate と同等 |
import { scheduler } from "node:timers/promises" |
| 未対応のブラウザ | フォールバックが必要 | 下のコード |
未対応の環境に備えて、次のような関数を用意しておくと安心です。
// どの環境でも使える「いったん譲る」関数
function yieldToMain() {
if (globalThis.scheduler?.yield) return scheduler.yield();
return new Promise((resolve) => setTimeout(resolve, 0));
}
async function processAll(items, handle) {
let last = performance.now();
for (const item of items) {
handle(item);
if (performance.now() - last > 50) { // 50msごとに譲る
await yieldToMain();
last = performance.now();
}
}
}
毎回譲るのではなく「50msごと」に譲ると、合計時間をほとんど延ばさずに画面の応答を保てます。 50msは、ブラウザが「長いタスク(Long Task)」とみなす基準と同じ値です。
よくあるつまずき
awaitの後は別タスクではない
await の続きはマイクロタスクで、同じタスクの中で動きます。 そのため次の点が起きます。
| 思い込み | 実際 | 確認した箇所 |
|---|---|---|
await を挟むと描画される |
描画されない | 分割の実測B(最長1000ms止まる) |
await するとほかのタイマーが先に動く |
タイマーはマイクロタスクがすべて終わるまで待つ | Q5(timeout が最後) |
async 関数は呼んだ時点で後回しになる |
最初の await までは同期 |
Q2(A が 2 より先) |
マイクロタスクの無限ループで画面が固まる
マイクロタスクの中で新しいマイクロタスクを登録し続けると、キューが空にならず、タスクも描画も止まります。 1秒で止まる連鎖を作って、ブラウザとNode.jsで確かめました。
// ブラウザ(Chromium 141): 1秒間 queueMicrotask を連鎖させる
const t0 = performance.now();
let frames = 0, count = 0, framesWhileLooping = 0;
requestAnimationFrame(function f() { frames++; if (performance.now() - t0 < 1500) requestAnimationFrame(f); });
setTimeout(() => console.log({ timeoutAt: (performance.now() - t0).toFixed(0) + " ms", microtasks: count, framesWhileLooping }), 0);
function loop() {
count++;
if (performance.now() - t0 < 1000) queueMicrotask(loop);
else framesWhileLooping = frames;
}
queueMicrotask(loop);
{ timeoutAt: '1001 ms', microtasks: 1072565, framesWhileLooping: 0 }
// starve_node.cjs(Node.js 22): 0.5秒間 process.nextTick を連鎖させる
const t0 = Date.now();
setTimeout(() => console.log("setTimeout(0) が動いた:", Date.now() - t0, "ms後"), 0);
let n = 0;
function loop() { n++; if (Date.now() - t0 < 500) process.nextTick(loop); else console.log("nextTickの連鎖が終了:", n, "回"); }
process.nextTick(loop);
nextTickの連鎖が終了: 3422184 回
setTimeout(0) が動いた: 507 ms後
setTimeout(0) は連鎖が終わるまで動かず、描画は0回でした。 終わりのない連鎖にすると、この状態がずっと続きます。くり返し処理は setTimeout や scheduler.yield() でタスクに戻しながら進めると、画面と入力が動き続けます。
そのほかのつまずき
| つまずき | 起きること | こうするとよい |
|---|---|---|
setTimeout(fn, 0) ですぐ動くと思う |
同期処理・マイクロタスクの後、さらに最小の遅延がつく | 「次のタスク」と考える。すぐ次なら queueMicrotask |
setTimeout の時間を正確だと思う |
指定は「最短の待ち時間」。前のタスクが長いと遅れる | 経過時間は performance.now() で測る |
| 順番の違うタスクソースに依存する | 環境で順番が変わる(Q9・Q10) | 順番が必要なら1つのPromiseチェーンにまとめる |
| Promiseのreturnの回数に依存する | 追加のマイクロタスクで順番がずれる(Q6) | 待ち合わせは await で明示する |
process.nextTick を多用する |
ESMとCJSで順番が変わる、I/Oが待たされる | 多くの場合は queueMicrotask で足りる |
よくある質問
Q. setTimeoutとPromiseはどちらが先に実行されますか?
A. Promiseの then(マイクロタスク)が先です。 setTimeout は遅延0でも次のタスクとして予約されるため、いまのタスクとマイクロタスクがすべて終わってから実行されます(クイズQ1・Q3)。
Q. マイクロタスクとマクロタスクの違いは何ですか?
A. マクロタスク(タスク)はイベントループが1回に1つだけ取り出す仕事、マイクロタスクは今のタスクの直後にキューが空になるまでまとめて実行される仕事です。 Promise・queueMicrotask・await の続きはマイクロタスク、setTimeout・イベント・MessageChannel はタスクです。
Q. process.nextTickとqueueMicrotaskはどちらを使えばよいですか?
A. 多くの場合は queueMicrotask がおすすめです。 ブラウザとNode.jsの両方で動き、Promiseと同じキューで順番がわかりやすいためです。process.nextTick はCommonJSとESモジュールで順番が変わります(クイズQ8の実測)。
Q. 重い処理で画面が固まるときはどうすればよいですか?
A. 処理を小分けにして、間に「タスクに戻す」待ちを入れます。 対応ブラウザでは scheduler.yield()、それ以外は setTimeout(0) や MessageChannel を使います。await null ではマイクロタスクのままなので描画は入りません。根本的に重い計算は Web Worker に移す方法もあります。
まとめ
- イベントループは「タスク1つ → マイクロタスク全部 → 描画 → 次のタスク」のくり返し
- Promise・
queueMicrotask・awaitの続きはマイクロタスクで、setTimeout(0)より必ず先 awaitは別タスクではない。区切っても描画は入らず、画面は止まったままprocess.nextTickはCommonJSではPromiseより先、ESモジュールのトップレベルでは後(Node.js 22で実測)- I/Oのコールバック内では
setImmediateがsetTimeout(0)より先。メインでは順番が毎回変わる - 重い処理は
scheduler.yield()・setTimeout・MessageChannelで分割。実測で描画間隔が983msから17msに - マイクロタスクの連鎖が続くとタスクも描画も止まる。くり返しはタスクに戻しながら進める
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JavaScriptイベントループ|マイクロタスクの実行順を図とクイズで」
株式会社RJCでは、JavaScriptのイベントループのしくみを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。