Interaction to Next Paint(INP)を理解する

1. はじめに

Interaction to Next Paint(INP)について学べるインタラクティブなデモと Codelab。

メインスレッドでのインタラクションを示す図。ブロッキング タスクの実行中にユーザーが入力を行います。入力はこれらのタスクが完了するまで遅延され、その後 pointerup、mouseup、クリック イベントのイベント リスナーが実行され、レンダリングとペイントの処理が開始されて次のフレームが表示されます。

前提条件

学習内容

  • ユーザー操作とそれらの操作の処理の相互作用がページの応答性にどのように影響するか。
  • 遅延を減らして解消し、スムーズなユーザー エクスペリエンスを実現する方法。

必要なもの

  • GitHub からコードのクローンを作成し、npm コマンドを実行できるパソコン。
  • テキスト エディタ。
  • すべてのインタラクション測定が機能する、Chrome の最新バージョン。

2. セットアップする

コードを取得して実行する

コードは web-vitals-codelabs リポジトリにあります。

  1. ターミナルでリポジトリのクローンを作成します。git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
  2. クローン作成したディレクトリ cd web-vitals-codelabs/understanding-inp に移動します。
  3. 依存関係をインストールします。npm ci
  4. ウェブサーバーを起動します。npm run start
  5. ブラウザで http://localhost:5173/understanding-inp/ にアクセスします。

アプリの概要

ページの上部には、[Score] カウンタと [Increment] ボタンがあります。反応性と応答性の典型的なデモです。

この Codelab のデモアプリのスクリーンショット

ボタンの下には、次の 4 つの測定値が表示されます。

  • INP: 現在の INP スコア。通常は最も悪いインタラクションです。
  • インタラクション: 直近のインタラクションのスコア。
  • FPS: ページのメインスレッドの 1 秒あたりのフレーム数。
  • タイマー: ジャンクを可視化するための、実行中のタイマーのアニメーション。

FPS と Timer のエントリは、インタラクションの測定にはまったく必要ありません。これらは、レスポンシブ性を視覚化しやすくするために追加されています。

試してみる

[Increment] ボタンを操作して、スコアが増加することを確認します。INP と Interaction の値は、増分ごとに変化しますか?

INP は、ユーザーが操作した瞬間から、ページがレンダリングされた更新をユーザーに実際に表示するまでの時間を測定します。

3. Chrome DevTools でインタラクションを測定する

DevTools を開くには、[その他のツール] > [デベロッパー ツール] メニューを使用するか、ページを右クリックして [検証] を選択するか、キーボード ショートカットを使用します。

[パフォーマンス] パネルに切り替えます。このパネルは、インタラクションの測定に使用します。

アプリの横に表示された DevTools の [Performance] パネルのスクリーンショット

次に、[パフォーマンス] パネルでインタラクションをキャプチャします。

  1. 録画ボタンを押します。
  2. ページを操作します([Increment] ボタンを押します)。
  3. 録画を停止します。

表示されたタイムラインに [Interactions] トラックが表示されます。左側の三角形をクリックして展開します。

DevTools のパフォーマンス パネルを使用してインタラクションを記録するデモのアニメーション

2 つのインタラクションが表示されます。スクロールするか W キーを長押しして、2 つ目の画像を拡大します。

DevTools の [Performance] パネルのスクリーンショット。パネル内のインタラクションにカーソルを合わせると、インタラクションの短いタイミングを示すツールチップが表示されます。

インタラクションにカーソルを合わせると、インタラクションが高速で、処理時間に時間がかからず、入力遅延と表示遅延が最小限に抑えられていることがわかります。正確な長さはマシンの速度によって異なります。

4. 長時間実行イベント リスナー

index.js ファイルを開き、イベント リスナー内の blockFor 関数のコメント化を解除します。

コード全体を見る: click_block.html

button.addEventListener('click', () => {
  blockFor(1000);
  score.incrementAndUpdateUI();
});

ファイルを保存します。サーバーが変更を検出し、ページを更新します。

ページをもう一度操作してみてください。インタラクションが著しく遅くなります。

パフォーマンス トレース

[Performance] パネルでもう一度記録して、どのように表示されるかを確認します。

[パフォーマンス] パネルでの 1 秒間のインタラクション

以前は短時間で完了していたインタラクションが、1 秒かかるようになりました。

インタラクションにカーソルを合わせると、時間のほとんどが「処理時間」に費やされていることがわかります。これは、イベント リスナー コールバックの実行に要した時間です。ブロッキング blockFor 呼び出しは完全にイベント リスナー内にあるため、そこで時間がかかっています。

5. テスト: 処理期間

イベント リスナーの処理を並べ替える方法を試して、INP への影響を確認します。

まず UI を更新する

JS 呼び出しの順序を入れ替えて、最初に UI を更新してからブロックするとどうなるでしょうか。

完全なコードを確認する: ui_first.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  blockFor(1000);
});

UI が以前よりも早く表示されるようになったことにお気づきでしょうか。この順序は INP スコアに影響しますか?

トレースを取得して、インタラクションを調べて、違いがあるかどうかを確認してみてください。

リスナーを分離する

処理を別のイベントリスナーに移動したらどうなるでしょうか?1 つのイベント リスナーで UI を更新し、別のリスナーでページをブロックする。

コード全体を見る: two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

[パフォーマンス] パネルではどのように表示されますか?

さまざまなイベントタイプ

ほとんどのインタラクションでは、ポインタ イベントやキーイベントから、ホバー、フォーカス/ぼかし、beforechange や beforeinput などの合成イベントまで、さまざまな種類のイベントが発生します。

実際のページの多くには、さまざまなイベントのリスナーがあります。

イベント リスナーのイベントタイプを変更するとどうなりますか?たとえば、click イベント リスナーの 1 つを pointerup または mouseup に置き換えます。

コード全体を見る: diff_handlers.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('pointerup', () => {
  blockFor(1000);
});

UI の更新なし

イベント リスナーから UI を更新する呼び出しを削除するとどうなりますか?

完全なコードを見る: no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});

6. 処理期間のテスト結果

パフォーマンス トレース: 最初に UI を更新する

完全なコードを確認する: ui_first.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  blockFor(1000);
});

ボタンをクリックしたときのパフォーマンス パネルの記録を見ると、結果が変わっていないことがわかります。UI の更新はブロックコードの前にトリガーされましたが、ブラウザはイベント リスナーが完了するまで、画面に描画された内容を実際には更新しませんでした。つまり、インタラクションの完了には 1 秒強かかりました。

[パフォーマンス] パネルに表示された 1 秒間のインタラクション

パフォーマンス トレース: リスナーを分離する

コード全体を見る: two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

機能的な違いはありません。インタラクションには 1 秒かかります。

クリック インタラクションを拡大すると、click イベントの結果として 2 つの異なる関数が呼び出されていることがわかります。

予想どおり、最初の UI の更新は非常に高速に実行されますが、2 番目の処理には 1 秒かかります。しかし、それらの影響の合計は、エンドユーザーにとって同じ遅いインタラクションになります。

この例の 1 秒間のインタラクションを拡大した図。最初の関数呼び出しが 1 ミリ秒未満で完了していることがわかります。

パフォーマンス トレース: さまざまなイベントタイプ

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('pointerup', () => {
  blockFor(1000);
});

これらの結果は非常に似ています。インタラクションは依然として 1 秒全体を要しますが、唯一の違いは、UI の更新のみを行う短い click リスナーが、ブロッキング pointerup リスナーの後に実行されるようになったことです。

この例では、1 秒間のインタラクションを拡大して表示しています。pointerup リスナーの後、クリック イベント リスナーが 1 ミリ秒未満で完了していることがわかります。

パフォーマンス トレース: UI の更新なし

完全なコードを見る: no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});
  • スコアは更新されませんが、ページは更新されます。
  • アニメーション、CSS 効果、デフォルトのウェブ コンポーネント アクション(フォーム入力)、テキスト入力、テキスト ハイライトはすべて引き続き更新されます。

この場合、ボタンはクリックされるとアクティブな状態になり、その後元に戻ります。これにはブラウザによるペイントが必要になるため、INP は依然として発生します。

イベント リスナーがメインスレッドを 1 秒間ブロックしてページの描画を妨げたため、インタラクションには 1 秒かかります。

[Performance] パネルで記録すると、以前と同じインタラクションが表示されます。

[パフォーマンス] パネルでの 1 秒間のインタラクション

重要なポイント

イベント リスナーで実行されるコードは、インタラクションを遅らせます。

  • これには、異なるスクリプトから登録されたリスナーや、リスナーで実行されるフレームワークまたはライブラリ コード(コンポーネントのレンダリングをトリガーする状態の更新など)が含まれます。
  • 自社のコードだけでなく、すべてのサードパーティ スクリプトも対象となります。

これはよくある問題です。

最後に、コードがペイントをトリガーしないからといって、ペイントが遅いイベント リスナーの完了を待たないわけではありません。

7. テスト: 入力遅延

イベント リスナーの外部で長時間実行されるコードはどうでしょうか。次に例を示します。

  • 読み込みが遅い <script> があり、読み込み中にページがランダムにブロックされた場合。
  • setInterval などの API 呼び出しが、ページを定期的にブロックしている。

イベント リスナーから blockFor を削除して、setInterval() に追加してみてください。

コード全体を見る: input_delay.html

setInterval(() => {
  blockFor(1000);
}, 3000);


button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

どうなるでしょうか。

8. 入力遅延テストの結果

コード全体を見る: input_delay.html

setInterval(() => {
  blockFor(1000);
}, 3000);


button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

setInterval ブロッキング タスクの実行中に発生したボタンクリックを記録すると、インタラクション自体でブロッキング作業が行われていなくても、長時間実行インタラクションが発生します。

このような長時間実行期間は、多くの場合、長いタスクと呼ばれます。

DevTools でインタラクションにカーソルを合わせると、インタラクション時間が処理時間ではなく、主に入力遅延に起因していることがわかります。

DevTools の [Performance] パネルに、1 秒間のブロッキング タスク、そのタスクの途中で発生したインタラクション、642 ミリ秒のインタラクション(ほとんどが入力遅延に起因)が表示されている

ただし、常にインタラクションに影響するわけではありません。タスクの実行中にクリックしなくても、運が良ければ成功するかもしれません。このような「ランダムな」くしゃみは、問題を引き起こすことがたまにあるだけで、デバッグが困難になることがあります。

これらを追跡する方法の一つは、長いタスク(または長いアニメーション フレーム)と Total Blocking Time を測定することです。

9. プレゼンテーションが遅い

ここまで、入力遅延やイベント リスナーを介した JavaScript のパフォーマンスについて見てきましたが、Next Paint のレンダリングに影響するものは他に何があるでしょうか。

それは、高負荷なエフェクトでページを更新することです。

ページの更新がすぐに完了しても、ブラウザはレンダリングに多くの処理を必要とする場合があります。

メインスレッド:

  • 状態の変化後に更新をレンダリングする必要がある UI フレームワーク
  • DOM の変更や、負荷の高い CSS クエリ セレクタの切り替えを多数行うと、スタイル、レイアウト、ペイントが大量にトリガーされる可能性があります。

メインスレッド外:

  • CSS を使用して GPU エフェクトを強化する
  • 非常に大きな高解像度画像を追加する
  • SVG/Canvas を使用して複雑なシーンを描画する

ウェブでのレンダリングのさまざまな要素のスケッチ

RenderingNG

ウェブでよく見られる例をいくつか紹介します。

  • リンクをクリックした後に DOM 全体を再構築する SPA サイト。最初の視覚的なフィードバックを提供するために一時停止することはありません。
  • 動的なユーザー インターフェースを備えた複雑な検索フィルタを提供するが、そのために高コストのリスナーを実行する検索ページ。
  • ページ全体のスタイル/レイアウトをトリガーするダークモードの切り替え

10. テスト: 表示の遅延

requestAnimationFrameのパフォーマンス低下

requestAnimationFrame() API を使用して、長いプレゼンテーション遅延をシミュレートしてみましょう。

blockFor 呼び出しを requestAnimationFrame コールバックに移動して、イベント リスナーが戻った後に実行されるようにします。

コード全体を見る: presentation_delay.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

どうなるでしょうか。

11. 表示遅延テストの結果

コード全体を見る: presentation_delay.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

インタラクションは 1 秒のままです。何が起こったのでしょうか。

requestAnimationFrame は、次のペイントの前にコールバックをリクエストします。INP はインタラクションから次のペイントまでの時間を測定するため、requestAnimationFrame の blockFor(1000) は 1 秒間、次のペイントをブロックし続けます。

[パフォーマンス] パネルに表示された 1 秒間のインタラクション

ただし、次の 2 点に注意してください。

  • カーソルを合わせると、イベント リスナーが戻った後にメインスレッドのブロックが発生しているため、すべてのインタラクション時間が「プレゼンテーション遅延」に費やされていることがわかります。
  • メインスレッド アクティビティのルートは、クリック イベントではなく、[Animation Frame Fired] になっています。

12. インタラクションの診断

このテストページでは、スコア、タイマー、カウンタ UI を使用して、応答性が非常に視覚的に表示されますが、平均的なページをテストする場合は、より微妙です。

インタラクションが長く続く場合、原因が何であるかは必ずしも明確ではありません。

  • 入力遅延
  • イベント処理の期間
  • 表示の遅延

任意のページで、DevTools を使用して応答性を測定できます。この習慣を身につけるには、次のフローを試してください。

  1. 通常どおりにウェブを閲覧します。
  2. DevTools の [パフォーマンス] パネルのリアルタイム指標ビューで、インタラクション ログを監視します。
  3. パフォーマンスの低いインタラクションが見つかった場合は、そのインタラクションを繰り返してみてください。
  • 再現できない場合は、インタラクション ログを使用して分析情報を取得します。
  • 再現できる場合は、[Performance] パネルでトレースを記録します。

すべての遅延

これらの問題をすべて少しずつページに追加してみましょう。

コード全体を見る: all_the_things.html

setInterval(() => {
  blockFor(1000);
}, 3000);

button.addEventListener('click', () => {
  blockFor(1000);
  score.incrementAndUpdateUI();

  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

次に、コンソールとパフォーマンス パネルを使用して問題を診断します。

13. 演習: 非同期処理

ネットワーク リクエストの作成、タイマーの開始、グローバル状態の更新など、インタラクション内で視覚効果のない処理を開始できるため、それらの処理によって最終的にページが更新されるとどうなるでしょうか。

インタラクション後の次の描画のレンダリングが許可されている限り、ブラウザが新しいレンダリング更新は実際には必要ないと判断した場合でも、インタラクションの測定は停止します。

これを試すには、クリック リスナーから UI の更新を続行しますが、タイムアウトからブロッキング処理を実行します。

コード全体を見る: timeout_100.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

どうなりますか?

14. 非同期処理のテスト結果

コード全体を見る: timeout_100.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

27 ミリ秒のインタラクションと 1 秒の長いタスクがトレースの後半で発生

UI が更新された直後にメインスレッドが使用可能になるため、インタラクションが短くなりました。時間のかかるブロッキング タスクは、描画後に実行されるため、ユーザーはすぐに UI フィードバックを受け取ることができます。

教訓: 削除できない場合は、少なくとも移動しましょう。

メソッド

固定の 100 ミリ秒の setTimeout よりも改善できるでしょうか。コードをできるだけ早く実行したいという要望は変わらないはずです。そうでないなら、コードを削除すればよいだけです。

目標:

  • インタラクションは incrementAndUpdateUI() を実行します。
  • blockFor() はできるだけ早く実行されますが、次のペイントをブロックしません。
  • これにより、「マジック タイムアウト」なしで予測可能な動作が実現します。

これを実現する方法には、次のようなものがあります。

  • setTimeout(0)
  • Promise.then()
  • requestAnimationFrame
  • requestIdleCallback
  • scheduler.postTask()

"requestPostAnimationFrame"

requestAnimationFrame 単体では、次の描画の前に実行しようとするため、通常はインタラクションが遅くなります。これとは異なり、requestAnimationFrame + setTimeout は requestPostAnimationFrame のシンプルなポリフィルとして機能し、次の描画の後にコールバックを実行します。

コード全体を見る: raf+task.html

function afterNextPaint(callback) {
  requestAnimationFrame(() => {
    setTimeout(callback, 0);
  });
}

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  afterNextPaint(() => {
    blockFor(1000);
  });
});

人間工学の観点から、Promise でラップすることもできます。

完全なコードを見る: raf+task2.html

async function nextPaint() {
  return new Promise(resolve => afterNextPaint(resolve));
}

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();

  await nextPaint();
  blockFor(1000);
});

15. 複数のインタラクション(および怒りのクリック)

長時間ブロックする処理を移動すると、状況が改善されることもありますが、そうした長いタスクは依然としてページをブロックし、今後のインタラクションだけでなく、他の多くのページ アニメーションや更新にも影響します。

ページの非同期ブロッキング処理バージョンをもう一度試します(最後のステップで処理を遅らせる独自のバリエーションを思いついた場合は、独自のバージョンを試します)。

コード全体を見る: timeout_100.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

すばやく複数回クリックするとどうなりますか?

パフォーマンス トレース

クリックごとに 1 秒のタスクがキューに追加され、メインスレッドが長時間ブロックされます。

メインスレッドで複数の長いタスクが数秒間実行され、インタラクションが 800 ミリ秒遅くなる

これらの長いタスクが新しいクリックと重複すると、イベント リスナー自体はほぼすぐに戻るにもかかわらず、インタラクションが遅くなります。入力遅延に関する前のテストと同じ状況を作り出しました。ただし、この場合の入力遅延は setInterval によるものではなく、以前のイベント リスナーによってトリガーされた処理によるものです。

戦略

理想的には、長いタスクを完全に削除することです。

  • 不要なコード(特にスクリプト)をすべて削除します。
  • 長いタスクが実行されないようにコードを最適化する。
  • 新しいインタラクションが到着したときに、古い処理を中止する。

16. 戦略 1: デバウンス

古典的な戦略です。インタラクションが連続して発生し、処理やネットワークへの影響が大きい場合は、意図的に処理の開始を遅らせて、キャンセルと再開ができるようにします。このパターンは、予測入力フィールドなどのユーザー インターフェースに役立ちます。

  • setTimeout を使用して、タイマー(500 ~ 1,000 ミリ秒など)で負荷の高い処理の開始を遅らせます。
  • その際、タイマー ID を保存します。
  • 新しいインタラクションが到着した場合は、clearTimeout を使用して以前のタイマーをキャンセルします。

コード全体を見る: debounce.html

let timer;
button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  if (timer) {
    clearTimeout(timer);
  }
  timer = setTimeout(() => {
    blockFor(1000);
  }, 1000);
});

パフォーマンス トレース

複数のインタラクションがあるが、それらすべてを処理する長いタスクが 1 つだけある

複数回クリックしても、実行される blockFor タスクは 1 つだけで、クリックが 1 秒間行われなくなるまで待機してから実行されます。テキスト入力での入力や、複数のクイッククリックが予想されるアイテム ターゲットなど、バーストで発生するインタラクションの場合、これはデフォルトで使用するのに最適な戦略です。

17. 戦略 2: 長時間実行される処理を中断する

デバウンス期間が経過した直後にクリックが発生し、長いタスクの途中で発生し、入力遅延により非常に遅いインタラクションになる可能性は依然としてあります。

理想的には、タスクの途中でインタラクションが発生した場合、作業を一時停止して、新しいインタラクションにすぐに対応できるようにしたいところです。どうすればよいでしょうか。

isInputPending などの API もありますが、一般的には、長いタスクをチャンクに分割する方が適切です。

大量の setTimeout

最初の試み: シンプルなことを行う。

コード全体を見る: small_tasks.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  requestAnimationFrame(() => {
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
  });
});

これは、ブラウザが各タスクを個別にスケジュールできるようにすることで機能し、入力の優先度を高くすることができます。

複数のインタラクションがあるが、スケジュールされたすべての作業が多数の小さなタスクに分割されている

5 回のクリックで 5 秒間の処理を行うという点では元に戻っていますが、クリックごとに 1 秒のタスクが 100 ミリ秒のタスク 10 個に分割されています。その結果、複数のインタラクションがこれらのタスクと重複していても、100 ミリ秒を超える入力遅延が発生するインタラクションはありません。ブラウザは、setTimeout の処理よりも受信したイベント リスナーを優先し、インタラクションの応答性を維持します。

この戦略は、別々のエントリ ポイントをスケジュールする場合に特に有効です。たとえば、アプリケーションの読み込み時に呼び出す必要がある独立した機能が多数ある場合などです。スクリプトを読み込んで、スクリプトの評価時にすべてを実行すると、デフォルトでは、すべてが巨大な長いタスクで実行される可能性があります。

ただし、この戦略は、共有状態を使用する for ループなど、密結合されたコードを分割する場合にはうまく機能しません。

yield() を利用できるようになりました

しかし、最新の async と await を活用することで、任意の JavaScript 関数に「明け渡し点」を簡単に追加できます。

次に例を示します。

コード全体を確認する: yieldy.html

// Polyfill for scheduler.yield()
async function schedulerDotYield() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function blockInPiecesYieldy(ms) {
  const ms_per_part = 10;
  const parts = ms / ms_per_part;
  for (let i = 0; i < parts; i++) {
    await schedulerDotYield();

    blockFor(ms_per_part);
  }
}

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();
  await blockInPiecesYieldy(1000);
});

以前と同様に、メインスレッドは一連の処理の後に解放され、ブラウザは受信したインタラクションに応答できます。ただし、必要なのは個別の setTimeout ではなく await schedulerDotYield() のみであるため、for ループの途中でも使用できるほど人間工学的です。

AbortContoller() を利用できるようになりました

この方法は機能しますが、新しいインタラクションが届いて、行うべき作業が変更された可能性がある場合でも、各インタラクションでさらに多くの作業がスケジュールされます。

デバウンス戦略では、新しいインタラクションが発生するたびに、以前のタイムアウトをキャンセルしました。ここでも同じようなことができますか?その方法の一つとして、AbortController() を使用できます。

完全なコードを見る: aborty.html

// Polyfill for scheduler.yield()
async function schedulerDotYield() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function blockInPiecesYieldyAborty(ms, signal) {
  const parts = ms / 10;
  for (let i = 0; i < parts; i++) {
    // If AbortController has been asked to stop, abandon the current loop.
    if (signal.aborted) return;

    await schedulerDotYield();

    blockFor(10);
  }
}

let abortController = new AbortController();

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();

  abortController.abort();
  abortController = new AbortController();

  await blockInPiecesYieldyAborty(1000, abortController.signal);
});

クリックが発生すると、blockInPiecesYieldyAborty for ループが開始され、必要な作業が実行されます。その間、メインスレッドが定期的に解放されるため、ブラウザは新しいインタラクションに応答し続けることができます。

2 回目のクリックが発生すると、最初のループは AbortController でキャンセル済みとしてフラグが付けられ、新しい blockInPiecesYieldyAborty ループが開始されます。最初のループが次に実行されるようにスケジュールされると、signal.aborted が true になっていることが検出され、それ以上の処理を行わずにすぐに戻ります。

メインスレッドの処理が細かく分割され、インタラクションが短くなり、処理は必要な時間だけ継続する

18. まとめ

すべての長いタスクを分割すると、サイトは新しいインタラクションに応答できるようになります。これにより、最初のフィードバックを迅速に提供できるだけでなく、進行中の作業を中止するなどの判断も可能になります。場合によっては、エントリ ポイントを個別のタスクとしてスケジュールする必要があります。場合によっては、都合の良い場所に「譲る」ポイントを追加することを意味します。

重要ポイント

  • INP はすべてのインタラクションを測定します。
  • 各インタラクションは、入力から次のペイントまで測定されます。これは、ユーザーが応答性を確認する方法です。
  • 入力遅延、イベント処理時間、表示遅延はすべて、インタラクションの応答性に影響します。
  • DevTools を使用すると、INP とインタラクションの内訳を簡単に測定できます。

戦略

  • ページに長時間実行されるコード(長いタスク)を配置しない。
  • 不要なコードをイベント リスナーから次のペイントまで移動します。
  • レンダリングの更新自体がブラウザにとって効率的であることを確認します。

詳細