isInputPending() を使用した JS スケジューリングの改善

読み込みパフォーマンスと入力の応答性のトレードオフを回避できる新しい JavaScript API。

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

高速読み込みは難しいものです。現在、JS を使用してコンテンツをレンダリングするサイトでは、読み込みパフォーマンスと入力の応答性とのトレードオフを行う必要があります。表示に必要なすべての処理を一度に実行する(読み込みパフォーマンスは向上するが、入力の応答性は低下する)、または入力とペイントに応答し続けるために処理を小さなタスクに分割する(読み込みパフォーマンスは低下するが、入力の応答性は向上する)のいずれかです。

このトレードオフの必要性をなくすため、Facebook は収益を生むことなく応答性を向上させるために、Chromium に isInputPending() API を提案して実装しました。オリジン トライアルのフィードバックに基づいて API を複数回更新し、Chromium 87 で API がデフォルトで提供されるようになりました。

ブラウザの互換性

対応ブラウザ

  • Chrome: 87。
  • Edge: 87。
  • Firefox: サポートされていません。
  • Safari: サポートされていません。

ソース

isInputPending() は、バージョン 87 以降の Chromium ベースのブラウザでリリースされています。どのブラウザでも、API を配布する意思は示されていません。

背景

現在の JS エコシステムでは、ほとんどの処理がメインスレッドという単一のスレッドで実行されます。これにより、デベロッパーは堅牢な実行モデルを利用できますが、スクリプトが長時間実行されると、ユーザー エクスペリエンス(特に応答性)が大幅に低下する可能性があります。たとえば、入力イベントが発生しているときにページで大量の処理が行われている場合、その処理が完了するまで、ページはクリック入力イベントを処理しません。

現在のベスト プラクティスは、JavaScript を小さなブロックに分割してこの問題に対処することです。ページの読み込み中に、ページは少しの JavaScript を実行し、ブラウザに制御を渡すことができます。ブラウザは入力イベント キューをチェックし、ページに伝える必要があるものがないか確認できます。その後、ブラウザは追加された JavaScript ブロックの実行に戻ることができます。これは役立ちますが、他の問題を引き起こす可能性があります。

ページがブラウザに制御を返すたびに、ブラウザが入力イベントキューをチェックし、イベントを処理して、次の JavaScript ブロックを取得するまでに時間がかかります。ブラウザはイベントに迅速に応答しますが、ページの全体的な読み込み時間は遅くなります。頻繁に降伏すると、ページの読み込みが遅くなります。頻繁に譲渡しないと、ブラウザがユーザー イベントに応答するまでに時間がかかり、ユーザーの不満が増大します。楽しくなかった。

長い JS タスクを実行すると、ブラウザがイベントをディスパッチする時間が減ることを示す図。

Facebook では、この不満の多いトレードオフを排除する新しい読み込み方法を思いついたらどうなるかを検討しました。この件について Chrome の担当者に連絡し、isInputPending() の提案をまとめました。isInputPending() API は、ウェブでのユーザー入力に割り込みのコンセプトを初めて使用し、JavaScript がブラウザに譲渡することなく入力を確認できるようにします。

isInputPending() を使用すると、JS は実行をブラウザに完全に譲渡することなく、保留中のユーザー入力があるかどうかを確認できます。

この API に関心が寄せられたため、Chrome の同僚と連携して Chromium にこの機能を実装し、リリースしました。Chrome エンジニアの協力を得て、オリジン トライアル(API を完全にリリースする前に、Chrome が変更をテストしてデベロッパーからフィードバックを得る方法)でパッチをリリースしました。

初期試用版と W3C ウェブ パフォーマンス ワーキング グループの他のメンバーからのフィードバックを受け、API の変更を実装しました。

例: yiey スケジューラ

ページの読み込みに、コンポーネントからのマークアップの生成、素数の因数分解、クールな読み込みスピナーの描画など、表示をブロックする作業が多数あるとします。これらの各タスクは個別の作業項目に分割されます。スケジューラ パターンを使用して、仮定の processWorkQueue() 関数で作業を処理する方法を図示します。

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (performance.now() >= DEADLINE) {
    // Yield the event loop if we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

setTimeout() を介して新しいマクロタスクの後半で processWorkQueue() を呼び出すことで、ブラウザは比較的中断されることなく実行しながら、入力にもある程度応答できるようになります(処理が再開される前にイベント ハンドラを実行できます)。ただし、イベントループの制御を必要とする他の処理によって長時間スケジュール解除されたり、イベント レイテンシが最大 QUANTUM ミリ秒余分に増加したりする可能性があります。

問題ありませんが、さらに改善する余地はありますか?もちろんです

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending() || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event, or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

navigator.scheduling.isInputPending() の呼び出しを導入することで、ディスプレイのブロック作業が中断されることなく実行される一方で、入力に迅速に対応できるようになりました。処理が完了するまで入力(ペイントなど)以外の処理を行う必要がない場合は、QUANTUM の長さを簡単に増やすこともできます。

デフォルトでは、isInputPending() から「連続」イベントは返されません。たとえば、mousemovepointermove などです。これらの動画の収益化にもご興味をお持ちの場合は、お気軽にお問い合わせください。includeContinuoustrue に設定したオブジェクトを isInputPending() に渡すと、準備が整います。

const DEADLINE = performance.now() + QUANTUM;
const options = { includeContinuous: true };
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending(options) || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event (any of them!), or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

これで、React などのフレームワークでは、同様のロジックを使用してコア スケジューリング ライブラリに isInputPending() サポートが組み込まれています。これにより、これらのフレームワークを使用するデベロッパーは、大幅な書き換えを行うことなく、バックグラウンドで isInputPending() のメリットを享受できるようになります。

譲歩は必ずしも悪いことではありません

ただし、より少ない量を割り当てることがすべてのユースケースに適したソリューションであるとは限りません。入力イベントを処理する以外にも、レンダリングの実行やページ上の他のスクリプトの実行など、ブラウザに制御を返す理由はたくさんあります。

ブラウザが保留中の入力イベントを適切に関連付けられない場合があります。特に、クロスオリジン iframe に複雑なクリップとマスクを設定すると、誤検出が発生する可能性があります(つまり、これらのフレームをターゲットにすると、isInputPending() が予期せず false を返す可能性があります)。サイトがスタイル設定されたサブフレームとのインタラクションを必要とする場合は、十分な頻度でイジェクトするようにしてください。

イベントループを共有する他のページにも注意してください。Chrome for Android などのプラットフォームでは、複数のオリジンがイベントループを共有することがよくあります。入力がクロスオリジン フレームにディスパッチされた場合、isInputPending()true を返すことはありません。そのため、バックグラウンド ページがフォアグラウンド ページの応答性に干渉する可能性があります。Page Visibility API を使用してバックグラウンドで作業を実行する場合、作業を減らす、延期する、収益を増やすなどの対策を講じることができます。

isInputPending() は慎重に使用してください。ユーザーをブロックする処理が不要な場合は、イベントループで他の処理に優先して、より頻繁に yield を実行します。長いタスクは有害な場合があります

フィードバック

  • is-input-pending リポジトリで仕様に関するフィードバックを送信します。
  • Twitter で @acomminos(仕様作成者の 1 人)に連絡します。

まとめ

isInputPending() がリリースされ、デベロッパーが本日より使用できるようになることを嬉しく思います。この API は、Facebook が新しいウェブ API を構築し、アイデアの検討から標準提案、実際にブラウザでリリースするまでのプロセスを初めて行ったものです。ここまで到達できたのは、皆様のご協力のおかげです。このアイデアを具体化してリリースに至るまでサポートしてくれた Chrome の皆様に、特に感謝の意を表します。

ヒーロー写真: Will H McMahanUnsplash