🕰️

長いタスクを分割するscheduler.yieldという提案

2023/09/07に公開

3 行まとめ

  • scheduler.yieldが Chrome115 から origin trial で試せるように
  • scheduler.yieldを使うと長いタスクを分割できる
  • scheduler.yieldではユーザーのインタラクション以外のタスクが割り込まない

Long task の問題とタスクの分割

ブラウザのメインスレッドを占有するような実行時間が長いタスク(Long task)は、そのタスクが実行されている間に何かしらのインタラクションがあっても、タスクが終わるまでブラウザはインタラクションに対応できません。

クリックがあっても長いタスクが終わるまではクリックのタスクは実行できない
クリックしても長いタスクが終わるまではクリックのタスクは実行されない

こういった長いタスクを分割することで、インタラクションなどの優先度の高いタスクに対応することができます。これは Core Web Vitals の指標であるInteraction to Next Paint(INP)の向上も期待できます。

タスクを分割するとクリックのタスクを間に差し込める
タスク分割をすることでクリックのタスクが次のタスクとして実行される

async/awaitsetTimeoutを使ったタスクの分割

長いタスクを分割する方法として、async/awaitsetTimeoutを使った方法があります。

実際にフォーム画面で submit した際の一連の長い処理を例にタスクの分割をみてみましょう。

この例では submit された際に、以下の処理を行います。

  1. バリデーション
  2. ローディングの表示
  3. 入力内容を保存する API へ送信
  4. submit 後の画面の更新
  5. アナリティクスの送信
function validateForm() {
  // バリデーション
}
function showSpinner() {
  // submit後のローディングの表示
}
function saveToDatabase() {
  // 入力内容を保存するAPIへ送信
}
function updateUI() {
  // submit後の画面の更新
}
function sendAnalytics() {
  // アナリティクスの送信
}

function handleSubmit() {
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}

タスクの実行状況は Chrome DevTools の Performance タブ上で確認することもできます。

Chrome DevTools上に表示されるLong task

この長いタスクを async/awaitsetTimeoutを使って分割してみましょう。

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文の中では、関数が実行された後にyieldToMainawaitします。yieldToMainでは、setTimeoutを使って 0ms 後にresolveするPromiseを返しています。

async/awaitsetTimeoutを使うことで、タスクが Chrome 上でどのように実行されているのか確認してみましょう。

Chrome DevTools上に表示される分割されたタスク

Performance タブを見ると関数ごとにタスクが分割されていることがわかります。

さらに分割されたタスクを見てみると、最初の関数であるvalidateFormは Click 時に実行され、それ以降の関数はsetTimeoutによるタイマー起動時(Timer Fired)に Microtask として実行されています。

長いタスクが分割される仕組み

async/awaitsetTimeoutを使うと、なぜタスクが分割されるのか順番に見ていきましょう。

まず、フォームが submit された際にhandleSubmitが実行されます。handleSubmitの中では最初の関数であるvalidateFormが実行されます。

step1

次に、handleSubmityieldToMainが実行され、setTimeoutによって 0ms 後にresolveするPromiseが返されます。この時、setTimeoutのコールバックの処理は Task Queue に入れられます。

step2

yieldToMainawaitしているので、返ってくるPromiseが完了するまでhandleSubmitの処理は中断されます。

step3

次のタスクとして、Task Queue にあるsetTimeoutのコールバックの処理が実行されます。

step4

setTimeoutのコールバックの処理が終わると、yieldToMainPromiseresolveされるので、handleSubmitの後続の処理が再開されます。for文の中では次の関数であるshowSpinneryieldToMainが Microtask Queue に入れられます。

step5

Microtask Queue に入れられたshowSpinneryieldToMainが実行されます。

step6

yieldToMainが実行され、setTimeoutのコールバックの処理は Task Queue に入れられます。(以降、関数分、この流れが繰り返される)

step7

async/awaitsetTimeoutを使った際の問題点

async/awaitsetTimeoutを使って長いタスクを分割した場合、ユーザーのインタラクション以外のタスクが、分割したタスクの間に割り込んでしまうことがあります。

例えば、サードパーティのスクリプトがsetIntervalを使い、一定の間隔で何かしらの処理をしていた場合などが挙げられます。実際にsetIntervalを実行中にasync/awaitsetTimeoutを使ったタスク分割を行うとsetIntervalのタスクが間に割り込んでしまうのが Performance タブで確認できます。

長いタスクを分割した際にサードパーティのスクリプト等のタスクが間に割り込んでしまう

scheduler.yieldとは

前置きが長くなりましたが、今回紹介するscheduler.yieldを使うと、長いタスクを分割する際にユーザーのインタラクション以外のタスクが、分割したタスクの間に割り込まないようにできます。

https://developer.chrome.com/blog/introducing-scheduler-yield-origin-trial/

scheduler.yieldは Chrome115 以降でフラグか origin trial で試せます。

使い方も簡単で、前述したサンプルコードのyieldToMainscheduler.yieldに置き換えるだけです。

  for (const task of tasks) {
    task();
-   await yieldToMain();
+   await scheduler.yield();
  }

実際にsetIntervalを実行中にscheduler.yieldを使ったタスク分割を行うと、setIntervalのタスクに割り込まれません。

scheduler.yieldを使ったタスク分割ではのタスクが割り込まない

DEMO

Chrome115 以上で origin trial のトークンが有効な間は、以下のリンクでscheduler.yieldの動作を確認できます。

https://nus3.github.io/ui-labs/scheduler-yield/

Start ボタンを押すと、setIntervalのタスクが一定間隔で実行します。その後、yielding setTimeout のボタンを押すとasync/awaitsetTimeoutを使ったタスク分割が実行され、setIntervalのタスクが間に割り込んでくるのを確認できます。

async、awaitとsetTimeoutによるタスク分割のデモ

また、Start ボタンを押してから yielding scheduler.yield のボタンを押すと、scheduler.yieldを使ったタスク分割が実行され、setIntervalのタスクが割り込まないことを確認できます。

scheduler.yieldによるタスク分割のデモ

ソースコードは以下のリポジトリで公開しています。

https://github.com/nus3/ui-labs/tree/main/src/scheduler-yield

あとがき

scheduler.yieldは、オリジントライアルかフラグ(chrome://flags/)を設定することで Chrome 上で気軽に使えます。また、フィードバックも募集しているようなので、興味がある人はぜひ試してみてください。

GitHubで編集を提案
サイボウズ フロントエンド

Discussion