- カテゴリー
- Web標準API
- 公開日
- 2026.09.26
Contents
はじめに
大量データをメインスレッドで一度に処理すると、クリックや描画も同じスレッドで待たされます。Prioritized Task Scheduling APIは、scheduler.yield()で処理を一時中断したり、scheduler.postTask()で優先度付きタスクを登録したりできます。
この記事では1件ずつJSONを整形する例を使います。CPU処理そのものを高速化するAPIではありません。
なぜ画面が固まるのか
多くのJavaScript、クリック処理、画面描画はメインスレッドで順番に実行されます。長いループがメインスレッドを使い続けると、利用者がボタンを押しても処理の順番が来ません。yield()は長い処理の途中でブラウザへいったん順番を返します。
- メインスレッド:画面描画や多くのJavaScriptを実行する中心の処理場所
- Long Task:メインスレッドを長時間占有し、操作遅延の原因になる処理
- AbortSignal:処理の中止要求を関数へ伝える仕組み
- フォールバック:新しいAPIが使えない環境で代わりに使う処理
必要環境と起動
Node.js 22.12以上とnpm、ブラウザを用意します。新しいフォルダーに次のindex.htmlを保存します。後述の3つのJavaScriptブロックは順番に1つのapp.jsへ保存してください。
<!doctype html>
<html lang="ja">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Scheduler API検証</title>
<h1>Scheduler API検証</h1>
<button id="start">処理開始</button>
<button id="stop" disabled>中断</button>
<button id="click">クリック確認</button>
<p id="clicks">クリック: 0回</p>
<p id="result" role="status">未実行</p>
<script type="module" src="./app.js"></script>
</html>
同じフォルダーでnpx --yes vite@7.3.6 --host 127.0.0.1を実行し、表示されたURLを開きます。初回はnpmからViteを取得します。終了はCtrl+Cです。APIのないブラウザではタイマーへフォールバックします。ただしタイマーは優先度を再現しません。
yieldで長いループを分割する
async function yieldToMain() {
if (globalThis.scheduler?.yield) {
await scheduler.yield();
} else {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
async function processRows(rows, { signal, chunkSize = 200 } = {}) {
if (!Array.isArray(rows)) throw new TypeError("rowsは配列にしてください");
if (!Number.isSafeInteger(chunkSize) || chunkSize < 1) {
throw new RangeError("chunkSizeは1以上の安全な整数にしてください");
}
signal?.throwIfAborted();
const results = [];
for (let i = 0; i < rows.length; i++) {
signal?.throwIfAborted();
results.push(JSON.stringify(rows[i]));
if ((i + 1) % chunkSize === 0) {
await yieldToMain();
}
}
signal?.throwIfAborted();
return results;
}
chunkSizeは端末と処理内容で調整します。件数では処理時間が安定しない場合、performance.now()を使い、およそ8~16msごとにyieldする方法もあります。この値は万能な推奨値ではなく測定の出発点です。
postTaskで優先度を付ける
async function scheduleAnalytics(send, signal) {
if (globalThis.scheduler?.postTask) {
return scheduler.postTask(send, {
priority: "background",
signal,
});
}
signal?.throwIfAborted();
return send();
}
const controller = new AbortController();
await scheduleAnalytics(() => console.log("低優先度の処理を実行"), controller.signal);
優先度はuser-blocking、user-visible、backgroundです。重要な作業をすべてuser-blockingにすると優先順位の意味がなくなります。
動的に優先度を変更する場合はTaskControllerを利用できますが、対応状況を確認します。単純なキャンセルだけならAbortControllerを渡す設計が理解しやすいでしょう。
ボタンから実行する
次も同じapp.jsの末尾へ追加します。処理中に「クリック確認」「中断」を操作できます。入力データの作成自体は同期処理なので、この小さなデモでも無制限に件数を増やさないでください。
const start = document.querySelector("#start");
const stop = document.querySelector("#stop");
const result = document.querySelector("#result");
let active;
let clicks = 0;
document.querySelector("#click").addEventListener("click", () => {
document.querySelector("#clicks").textContent = `クリック: ${++clicks}回`;
});
stop.addEventListener("click", () => active?.abort());
start.addEventListener("click", async () => {
if (active) return;
active = new AbortController();
start.disabled = true;
stop.disabled = false;
result.textContent = "処理中";
try {
const rows = Array.from({ length: 500000 }, (_, i) => ({ id: i }));
const output = await processRows(rows, { signal: active.signal });
result.textContent = `完了: ${output.length}件`;
} catch (error) {
result.textContent = error.name === "AbortError" ? "中断しました" : error.message;
} finally {
active = undefined;
start.disabled = false;
stop.disabled = true;
}
});
yieldとpostTaskの使い分け
| 目的 | API |
|---|---|
| 実行中のasync関数を途中で分割 | scheduler.yield() |
| 後で行う処理へ優先度を付ける | scheduler.postTask() |
| 重い処理を別スレッドへ移す | Web Worker |
| GPUで並列計算 | WebGPUなど別のAPI |
yieldを入れても合計CPU時間は減りません。数百ms以上の純粋計算や画像処理は、Web Workerへ移せないか先に検討します。
動作確認
Performanceパネルで分割前後を記録し、Long Taskの長さと入力応答を比較します。処理時間だけでなく、ボタンを連打したときに入力が反映されるかも確認します。
- 0件でも正常終了する
- 処理中にabortすると中断する
- 非対応環境でフォールバックが動く
- 途中で例外が起きたときPromiseがrejectされる
- 分割しすぎて総時間が大幅に悪化していない
簡単に違いを見るには、数万件の配列を用意し、処理中にも押せる「クリック確認」ボタンを画面へ置きます。分割前はボタン表示の更新が処理終了まで遅れ、分割後は途中で反映されれば改善を体感できます。端末性能で結果が変わるため、件数やchunkSizeを変えて測定してください。
よくあるトラブル
scheduler is not defined:機能検出をせず直接呼び出している。記事のyieldToMain()を経由するyield()を入れても総時間が短くならない:目的は高速化ではなく入力・描画を割り込ませることである- 画面がまだ固まる:1件の処理自体が長い、または
chunkSizeが大きすぎる - 分割後に極端に遅くなる:細かく分けすぎて切り替え回数が増えている
- 中止できない:ループ内で
signal.throwIfAborted()を定期的に実行する
画像変換や巨大な計算など、1処理そのものが重い場合はWeb Workerへの移動を優先します。
関連記事
まとめ
scheduler.yield()は長いasync処理の分割、postTask()は今後実行する処理の優先度とキャンセルに向きます。機能検出を行い、Performanceパネルと実機入力で効果を確認してください。


コメント