ユーザー入力にすばやく応答するウェブサイトの構築は、ウェブ パフォーマンスの最も難しい側面の 1 つであり、Chrome チームはウェブ デベロッパーがこの課題を解決できるよう支援に取り組んでいます。今年、Interaction to Next Paint(INP)指標が試験運用版から保留ステータスに移行することが発表されました。2024 年 3 月には、ウェブに関する主な指標としてFirst Input Delay(FID)に置き換わる予定です。
ウェブ デベロッパーがウェブサイトをできるだけスピーディに構築できるよう支援する新しい API を提供する取り組みの一環として、Chrome チームは現在、Chrome バージョン 115 以降で オリジン トライアルscheduler.yieldを実施しています。scheduler.yield は、scheduler API に追加される予定の新しい機能です。従来の方法よりも簡単かつ効果的に、メインスレッドに制御を戻すことができます。
Yielding について
JavaScript は、タスクの処理に run-to-completion モデルを使用します。つまり、タスクがメインスレッドで実行される場合、完了するまで必要なだけ実行されます。タスクが完了すると、制御はメインスレッドに 戻され、メインスレッドはキュー内の次のタスクを処理できるようになります。
タスクが完了しない極端なケース(無限ループなど)を除き、Yielding は JavaScript のタスク スケジューリング ロジックの不可欠な要素です。Yielding は必ず発生しますが、いつ発生するかは問題です。早ければ早いほどよいでしょう。タスクの実行に時間がかかりすぎる場合(正確には 50 ミリ秒を超える場合)、長いタスクと見なされます。
長いタスクは、ブラウザがユーザー入力に応答する能力を遅らせるため、ページの応答性が低下する原因となります。長いタスクが発生する頻度が高く、実行時間が長くなるほど、ユーザーはページが遅いと感じたり、完全に壊れていると感じたりする可能性が高くなります。
ただし、コードがブラウザでタスクを開始したからといって、そのタスクが完了するまでメインスレッドに制御を戻す必要はありません。タスクで明示的に Yielding を行うことで、ページのユーザー入力に対する応答性を改善できます。これにより、タスクが分割され、次の利用可能なタイミングで完了します。これにより、他のタスクは長いタスクが完了するのを待つ必要がなくなり、メインスレッドでより早く実行できるようになります。
明示的に Yielding を行うと、ブラウザに「これから行う作業には時間がかかる可能性があることを理解しています。ユーザー入力や他の重要なタスクに応答する前に、その作業をすべて行う必要はありません」と伝えます。 これはデベロッパーのツールボックスにある貴重なツールであり、ユーザー エクスペリエンスの改善に大きく貢献します。
現在の Yielding 戦略の問題点
一般的な Yielding 方法では、タイムアウト値が setTimeout の 0 を使用します。これは、setTimeout に渡されたコールバックが、残りの作業を別のタスクに移動し、後で実行されるようにキューに入れるためです。ブラウザが自動的に
Yielding を行うのを待つのではなく、「この大きな作業を小さな単位に分割する」と伝えます。
ただし、setTimeout を使用した Yielding には、望ましくない副作用が生じる可能性があります。Yielding
ポイントの後に実行される作業がタスクキューの末尾に移動します。 ユーザー
インタラクションによってスケジュールされたタスクは、通常どおりキューの先頭に移動しますが、明示的に Yielding
を行った後に実行したかった残りの作業は、競合するソースからの他のタスクによってさらに遅延する可能性があります。
実際の動作を確認するには、こちらの Codepen デモを試すか、次の埋め込みバージョンで試してください。デモは、クリックできるボタンと、タスクが実行されたときにログを記録するボックスで構成されています。ページにアクセスしたら、次の操作を行います。
- [Run tasks periodically] というラベルの付いた一番上のボタンをクリックします。これにより、ブロッキング タスクが定期的に実行されるようにスケジュールされます。このボタンをクリックすると、タスクログに [Ran blocking task with
setInterval] というメッセージがいくつか表示されます。 - 次に、[Run loop, yielding with
setTimeouton each iteration] というラベルの付いたボタンをクリックします。
デモの下部にあるボックスに、次のような内容が表示されます。
Processing loop item 1
Processing loop item 2
Ran blocking task via setInterval
Processing loop item 3
Ran blocking task via setInterval
Processing loop item 4
Ran blocking task via setInterval
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
この出力は、setTimeout を使用して Yielding を行うときに発生する「タスクキューの末尾」の動作を示しています。実行されるループは 5
つのアイテムを処理し、各アイテムが処理された後に setTimeout を使用して Yielding を行います。
これはウェブでよくある問題を示しています。スクリプト(特にサードパーティ スクリプト)が、一定の間隔で作業を実行するタイマー関数を登録することは珍しくありません。setTimeout
を使用した Yielding に伴う「タスクキューの末尾」の動作は、他のタスクソースからの作業が、Yielding
後にループが実行する必要がある残りの作業よりも先にキューに入れられる可能性があることを意味します。
アプリケーションによっては、これが望ましい結果になる場合もそうでない場合もありますが、多くの場合、この動作が原因で、デベロッパーはメインスレッドの制御を簡単に放棄することをためらうことがあります。Yielding
は、ユーザー インタラクションをより早く実行できるため便利ですが、ユーザー
インタラクション以外の作業もメインスレッドで実行できるようになります。これは実際の問題ですが、scheduler.yield
で解決できます。
scheduler.yield の導入
scheduler.yield は、Chrome バージョン 15 以降で、試験運用版のウェブプラットフォーム機能としてフラグの背後で利用可能になっています。setTimeout で Yielding
がすでに実行されているのに、Yielding に特別な関数が必要なのはなぜですか?
Yielding は setTimeout の設計目標ではなく、タイムアウト値が 0
の場合でも、後で実行されるコールバックをスケジュールする際の優れた副作用でした。ただし、覚えておくべき重要な点は、setTimeout
を使用した Yielding では、残りの作業がタスクキューの末尾に送信されることです。 デフォルトでは、scheduler.yield
は残りの作業をキューの先頭に送信します。 つまり、Yielding
の直後に再開したかった作業は、他のソースからのタスク(ユーザー インタラクションを除く)よりも優先されます。
scheduler.yield は、呼び出されるとメインスレッドに Yielding し、Promise
を返す関数です。つまり、async 関数で await できます。
async function yieldy () {
// Do some work...
// ...
// Yield!
await scheduler.yield();
// Do some more work...
// ...
}
scheduler.yield の実際の動作を確認するには、次の操作を行います。
chrome://flagsに移動します。- [Experimental Web Platform features] 試験運用版を有効にします。この操作を行った後、Chrome を再起動する必要がある場合があります。
- デモページに移動するか、このリストの後に埋め込まれたバージョンを使用します。
- [Run tasks periodically] というラベルの付いた一番上のボタンをクリックします。
- 最後に、[Run loop, yielding with
scheduler.yieldon each iteration] というラベルの付いたボタンをクリックします。
ページの下部にあるボックスの出力は次のようになります。
Processing loop item 1
Processing loop item 2
Processing loop item 3
Processing loop item 4
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
setTimeout を使用して Yielding を行うデモとは異なり、ループは反復ごとに Yielding
を行いますが、残りの作業をキューの末尾ではなく先頭に送信します。これにより、ウェブサイトの入力の応答性を向上させるために Yielding
を行うことができますが、Yielding の後に完了したかった作業が遅延しないようにすることもできます。
やってみましょう
scheduler.yield に興味があり、試してみたい場合は、Chrome バージョン 115 以降で次の 2 つの方法で試すことができます。
- ローカルで
scheduler.yieldを試す場合は、Chrome のアドレスバーに「chrome://flags」と入力して Enter キーを押し、[Experimental Web Platform Features] セクションのプルダウンから [有効にする] を選択します。これにより、scheduler.yield(およびその他の試験運用版機能)は、Chrome のインスタンスでのみ使用できるようになります。 - 一般公開されているオリジンで実際の Chromium ユーザーに対して
scheduler.yieldを有効にする場合は、scheduler.yieldオリジン トライアルに登録する必要があります。これにより、提案された機能を一定期間安全に試すことができ、Chrome チームはこれらの機能が現場でどのように使用されているかについての貴重な情報を得ることができます。オリジン トライアルの仕組みについて詳しくは、こちらのガイドをご覧ください。
scheduler.yield を使用する方法は、実装していないブラウザを引き続きサポートしながら、目標によって異なります。公式のポリフィルを使用できます。ポリフィルは、次のような場合に便利です。
- アプリケーションで
scheduler.postTaskを使用してタスクをスケジュールしている。 - タスクと Yielding の優先度を設定できるようにしたい。
scheduler.postTaskAPI が提供するTaskControllerクラスを使用して、タスクをキャンセルまたは優先度を変更できるようにしたい。
この説明に当てはまらない場合は、ポリフィルが適していない可能性があります。その場合は、いくつかの方法で独自のフォールバックを実装できます。最初のアプローチでは、scheduler.yield が使用可能な場合は使用しますが、使用できない場合は setTimeout にフォールバックします。
// A function for shimming scheduler.yield and setTimeout:
function yieldToMain () {
// Use scheduler.yield if it exists:
if ('scheduler' in window && 'yield' in scheduler) {
return scheduler.yield();
}
// Fall back to setTimeout:
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
// Example usage:
async function doWork () {
// Do some work:
// ...
await yieldToMain();
// Do some other work:
// ...
}
これは機能しますが、scheduler.yield
をサポートしていないブラウザでは、「キューの先頭」の動作なしで Yielding が行われます。Yielding
をまったく行わない方がよい場合は、scheduler.yield が使用可能な場合は使用しますが、使用できない場合は Yielding
をまったく行わない別のアプローチを試すことができます。
// A function for shimming scheduler.yield with no fallback:
function yieldToMain () {
// Use scheduler.yield if it exists:
if ('scheduler' in window && 'yield' in scheduler) {
return scheduler.yield();
}
// Fall back to nothing:
return;
}
// Example usage:
async function doWork () {
// Do some work:
// ...
await yieldToMain();
// Do some other work:
// ...
}
scheduler.yield は、scheduler API
に追加されるエキサイティングな機能です。これにより、デベロッパーは現在の Yielding
戦略よりも簡単に応答性を改善できるようになります。scheduler.yieldが便利な API であると思われる場合は、調査にご協力いただき、改善方法についてフィードバックをお寄せください。
Unsplash の Jonathan Allison によるヒーロー画像。