長いタスクを分割するscheduler.yieldという提案
3 行まとめ
-
scheduler.yield
が Chrome115 から origin trial で試せるように -
scheduler.yield
を使うと長いタスクを分割できる -
scheduler.yield
ではユーザーのインタラクション以外のタスクが割り込まない
Long task の問題とタスクの分割
ブラウザのメインスレッドを占有するような実行時間が長いタスク(Long task)は、そのタスクが実行されている間に何かしらのインタラクションがあっても、タスクが終わるまでブラウザはインタラクションに対応できません。
クリックしても長いタスクが終わるまではクリックのタスクは実行されない
こういった長いタスクを分割することで、インタラクションなどの優先度の高いタスクに対応することができます。これは Core Web Vitals の指標であるInteraction to Next Paint(INP)の向上も期待できます。
タスク分割をすることでクリックのタスクが次のタスクとして実行される
async
/await
と setTimeout
を使ったタスクの分割
長いタスクを分割する方法として、async
/await
と setTimeout
を使った方法があります。
実際にフォーム画面で submit した際の一連の長い処理を例にタスクの分割をみてみましょう。
この例では submit された際に、以下の処理を行います。
- バリデーション
- ローディングの表示
- 入力内容を保存する API へ送信
- submit 後の画面の更新
- アナリティクスの送信
function validateForm() {
// バリデーション
}
function showSpinner() {
// submit後のローディングの表示
}
function saveToDatabase() {
// 入力内容を保存するAPIへ送信
}
function updateUI() {
// submit後の画面の更新
}
function sendAnalytics() {
// アナリティクスの送信
}
function handleSubmit() {
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
タスクの実行状況は Chrome DevTools の Performance タブ上で確認することもできます。
この長いタスクを async
/await
と setTimeout
を使って分割してみましょう。
function yieldToMain() {
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
async function handleSubmit() {
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics,
];
for (const task of tasks) {
task();
await yieldToMain();
}
}
それぞれの処理(関数)を配列に格納し、for
文で順番に実行しています。for
文の中では、関数が実行された後にyieldToMain
をawait
します。yieldToMain
では、setTimeout
を使って 0ms 後にresolve
するPromise
を返しています。
async
/await
と setTimeout
を使うことで、タスクが Chrome 上でどのように実行されているのか確認してみましょう。
Performance タブを見ると関数ごとにタスクが分割されていることがわかります。
さらに分割されたタスクを見てみると、最初の関数であるvalidateForm
は Click 時に実行され、それ以降の関数はsetTimeout
によるタイマー起動時(Timer Fired)に Microtask として実行されています。
長いタスクが分割される仕組み
async
/await
と setTimeout
を使うと、なぜタスクが分割されるのか順番に見ていきましょう。
まず、フォームが submit された際にhandleSubmit
が実行されます。handleSubmit
の中では最初の関数であるvalidateForm
が実行されます。
次に、handleSubmit
はyieldToMain
が実行され、setTimeout
によって 0ms 後にresolve
するPromise
が返されます。この時、setTimeout
のコールバックの処理は Task Queue に入れられます。
yieldToMain
はawait
しているので、返ってくるPromise
が完了するまでhandleSubmit
の処理は中断されます。
次のタスクとして、Task Queue にあるsetTimeout
のコールバックの処理が実行されます。
setTimeout
のコールバックの処理が終わると、yieldToMain
のPromise
がresolve
されるので、handleSubmit
の後続の処理が再開されます。for
文の中では次の関数であるshowSpinner
とyieldToMain
が Microtask Queue に入れられます。
Microtask Queue に入れられたshowSpinner
とyieldToMain
が実行されます。
yieldToMain
が実行され、setTimeout
のコールバックの処理は Task Queue に入れられます。(以降、関数分、この流れが繰り返される)
async
/await
とsetTimeout
を使った際の問題点
async
/await
とsetTimeout
を使って長いタスクを分割した場合、ユーザーのインタラクション以外のタスクが、分割したタスクの間に割り込んでしまうことがあります。
例えば、サードパーティのスクリプトがsetInterval
を使い、一定の間隔で何かしらの処理をしていた場合などが挙げられます。実際にsetInterval
を実行中にasync
/await
とsetTimeout
を使ったタスク分割を行うとsetInterval
のタスクが間に割り込んでしまうのが Performance タブで確認できます。
scheduler.yield
とは
前置きが長くなりましたが、今回紹介するscheduler.yield
を使うと、長いタスクを分割する際にユーザーのインタラクション以外のタスクが、分割したタスクの間に割り込まないようにできます。
scheduler.yield
は Chrome115 以降でフラグか origin trial で試せます。
使い方も簡単で、前述したサンプルコードのyieldToMain
をscheduler.yield
に置き換えるだけです。
for (const task of tasks) {
task();
- await yieldToMain();
+ await scheduler.yield();
}
実際にsetInterval
を実行中にscheduler.yield
を使ったタスク分割を行うと、setInterval
のタスクに割り込まれません。
DEMO
Chrome115 以上で origin trial のトークンが有効な間は、以下のリンクでscheduler.yield
の動作を確認できます。
Start ボタンを押すと、setInterval
のタスクが一定間隔で実行します。その後、yielding setTimeout のボタンを押すとasync
/await
とsetTimeout
を使ったタスク分割が実行され、setInterval
のタスクが間に割り込んでくるのを確認できます。
また、Start ボタンを押してから yielding scheduler.yield のボタンを押すと、scheduler.yield
を使ったタスク分割が実行され、setInterval
のタスクが割り込まないことを確認できます。
ソースコードは以下のリポジトリで公開しています。
あとがき
scheduler.yield
は、オリジントライアルかフラグ(chrome://flags/
)を設定することで Chrome 上で気軽に使えます。また、フィードバックも募集しているようなので、興味がある人はぜひ試してみてください。
Discussion