1. はじめに
Interaction to Next Paint(INP)について学習するためのインタラクティブなデモと Codelab。
前提条件
- HTML および JavaScript 開発の知識
- 推奨: INP のドキュメントをご覧ください。
学習内容
- ユーザー インタラクションの相互作用とそれらのインタラクションの処理が、ページの応答性にどのように影響するか。
- 遅延を軽減して排除し、スムーズなユーザー エクスペリエンスを実現する方法。
必要なもの
- GitHub からコードのクローンを作成し、npm コマンドを実行できるコンピュータ。
- テキスト エディタ。
- すべてのインタラクション測定が機能するための最新バージョンの Chrome。
2. セットアップする
コードを取得して実行する
コードは web-vitals-codelabs
リポジトリにあります。
- ターミナルでリポジトリのクローンを作成します。
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
- クローンが作成されたディレクトリ(
cd web-vitals-codelabs/understanding-inp
)に移動します。 - 依存関係をインストールします。
npm ci
- ウェブサーバーを起動します。
npm run start
- ブラウザで http://localhost:5173/understanding-inp/ にアクセスします。
アプリの概要
ページの上部に、[スコア] カウンタと [インクリメント] ボタンがあります。リアクティブと応答性の典型的なデモ
ボタンの下に、次の 4 つの測定値があります。
- INP: 現在の INP スコア。通常はインタラクションが最悪です。
- インタラクション: 直近のインタラクションのスコア。
- FPS: メインスレッドのページの 1 秒あたりのフレーム数。
- タイマー: ジャンクを視覚化するための実行中のタイマー アニメーション。
FPS エントリとタイマー エントリは、インタラクションの測定にまったく必要ありません。これらは、応答性を可視化しやすくするために追加されています。
試してみる
[インクリメント] ボタンを操作して、スコアが上がるのを確認します。INP と Interaction の値はインクリメントごとに変わりますか。
INP は、ユーザーが操作してからページにレンダリングされた更新が実際に表示されるまでの時間を測定します。
3. Chrome DevTools の操作を測定する
[その他のツール] から DevTools を開きます[デベロッパー ツール] メニューで、ページを右クリックして [検証] を選択するか、キーボード ショートカットを使用します。
インタラクションを測定するには、[掲載結果] パネルに切り替えます。
次に、[パフォーマンス] パネルでインタラクションをキャプチャします。
- 録画ボタンを押します。
- ページを操作します([インクリメント] ボタンを押します)。
- 録画を停止します。
表示されるタイムラインに、[インタラクション] トラックが表示されます。左側の三角形をクリックして展開します。
2 つのインタラクションが表示されます。2 つ目の画像を拡大するには、W キーをスクロールするか長押しします。
インタラクションにカーソルを合わせると、インタラクションが高速で、処理時間に時間がなく、入力遅延とプレゼンテーションの遅延の最小時間を確認できます。正確な時間はマシンの速度によって異なります。
4. 長時間実行イベント リスナー
index.js
ファイルを開き、イベント リスナー内の blockFor
関数のコメント化を解除します。
コード全体を見る: click_block.html
button.addEventListener('click', () => {
blockFor(1000);
score.incrementAndUpdateUI();
});
ファイルを保存します。サーバーが変更を認識し、ページを更新します。
もう一度ページを操作してみてください。操作が大幅に遅くなります。
パフォーマンス トレース
[パフォーマンス] パネルでもう一度録画をして、表示を確認します。
以前は短いインタラクションだったのに、今では 1 秒もかかっています。
インタラクションにカーソルを合わせると、その時間のほとんどが「処理時間」に費やされていることがわかります。これは、イベント リスナーのコールバックを実行するのにかかった時間です。ブロッキング blockFor
呼び出しは完全にイベント リスナー内にあるため、必要な時間があります。
5. テスト: 処理時間
イベント リスナーの作業を並べ替える方法を試して、INP への影響を確認します。
最初に UI を更新する
js の呼び出しの順序を入れ替えるとどうなるでしょうか?まず UI を更新してからブロックするとどうなるでしょうか。
コード全体を見る: ui_first.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
blockFor(1000);
});
先ほど UI が表示されていたことに気付きましたか。順序は INP スコアに影響しますか?
トレースを取り、インタラクションを調べて、違いがないか確認してください。
個別のリスナー
処理を別のイベント リスナーに移動するとどうなるでしょうか。一方のイベント リスナーで 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 秒強かかっていました。
パフォーマンス トレース: 個別のリスナー
完全なコードを見る: Two_click.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('click', () => {
blockFor(1000);
});
機能的に違いはありません操作に 1 秒かかる。
クリック操作にズームインすると、click
イベントの結果として 2 つの異なる関数が呼び出されていることがわかります。
予想どおり、1 つ目の処理(UI の更新)はごく短時間で実行され、2 つ目の処理は 1 秒かかっています。ただし、それらの影響が合算すると、エンドユーザーに対する操作が同じように遅くなります。
パフォーマンス トレース: さまざまなイベントタイプ
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('pointerup', () => {
blockFor(1000);
});
これらの結果はよく似ています。インタラクションが 1 秒もかからない。唯一の違いは、短い UI 更新専用の click
リスナーが、ブロックしている pointerup
リスナーの後に実行されるようになったことです。
パフォーマンス トレース: UI の更新なし
コード全体を見る: no_ui.html
button.addEventListener('click', () => {
blockFor(1000);
// score.incrementAndUpdateUI();
});
- スコアは更新されませんが、ページは更新されています。
- アニメーション、CSS エフェクト、デフォルトのウェブ コンポーネント アクション(フォーム入力)、テキスト入力、テキストのハイライト表示はすべて、引き続き更新されます。
この場合、ボタンはアクティブ状態になり、クリックされると元の状態に戻ります。そのため、ブラウザによるペイントが必要です。つまり、INP がまだあるということです。
イベント リスナーがメインスレッドを 1 秒間ブロックし、ページを描画できないようにしているため、操作には 1 秒かかります。
パフォーマンス パネルの録画を撮影すると、それまでの操作とほぼ同じ操作を示すことができます。
重要なポイント
いずれかのイベント リスナーでコードが実行されると、操作が遅延します。
- これには、さまざまなスクリプトから登録されたリスナーや、リスナーで実行されるフレームワークまたはライブラリ コード(コンポーネントのレンダリングをトリガーする状態更新など)が含まれます。
- 独自のコードだけでなく、すべてのサードパーティ スクリプト。
これはよくある問題です。
最後に、コードがペイントをトリガーしないからといって、遅いイベント リスナーの完了をペイントが待機しないわけではありません。
7. テスト: 入力遅延
イベント リスナーの外で長時間実行されるコードはどうなりますか?例:
<script>
の遅延読み込みによって、読み込み中にページがランダムにブロックされた場合。- ページを定期的にブロックする API 呼び出し(
setInterval
など)
イベント リスナーから 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 でインタラクションにカーソルを合わせると、インタラクション時間が処理時間ではなく主に入力遅延に関連付けられていることを確認できます。
常にインタラクションに影響するわけではありません。タスクの実行中にクリックしなかった場合でも、ラッキーです。このような「ランダム」くしゃみが問題を引き起こすだけの場合、デバッグは悪夢です。
これを追跡する 1 つの方法は、長いタスク(または長いアニメーション フレーム)と合計ブロック時間を測定することです。
9. 表示速度が遅い
ここまで、入力遅延またはイベント リスナーを使用して JavaScript のパフォーマンスを確認してきましたが、次のペイントには他に何が影響するでしょうか。
高額な効果を加えてページを更新しています。
ページがすぐに更新されたとしても、ブラウザはそれを表示するために努力しなければならない場合があります。
メインスレッドで以下を行います。
- 状態変化後に更新をレンダリングする必要がある UI フレームワーク
- DOM の変更や、コストの高い多くの CSS クエリ セレクタの切り替えによって、多くのスタイル、レイアウト、ペイントがトリガーされる可能性があります。
メインスレッド以外:
- CSS を使用して GPU エフェクトを強化
- 非常に大きな高解像度の画像を追加する
- SVG/Canvas を使用して複雑なシーンを描画する
ウェブでよく見かける例:
- リンクをクリックすると、最初の視覚的なフィードバック提供を中断することなく 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 秒間ブロックし続けます。
ただし、次の 2 点に留意してください。
- カーソルを合わせると、すべてのインタラクション時間が「プレゼンテーションの遅延」に費やされていることがわかりますメインスレッドのブロックはイベント リスナーが戻った後に行われているためです。
- メインスレッド アクティビティのルートは、クリック イベントではなく、「アニメーション フレームの呼び出し」になりました。
12. インタラクションの診断
このテストページでは、スコアとタイマー、カウンタの UI を備えた非常に視覚的な応答性がありますが、平均的なページをテストする場合は、より微妙なものです。
インタラクションが長くなると、問題の原因が必ずしも明らかになるとは限りません。もしかして:
- 入力遅延はあるか?
- イベント処理の所要時間は?
- 表示が遅れますか?
任意のページで DevTools を使用して応答性を測定できます。この習慣を身につけるには、次のフローを試してみてください。
- 通常どおりにウェブを操作します。
- 省略可: Web Vitals 拡張機能がインタラクションをログに記録している間、DevTools コンソールを開いたままにします。
- パフォーマンスの低いインタラクションを見つけた場合は、繰り返してみてください。
- 繰り返しできない場合は、コンソールログを使用して分析情報を取得してください。
- 繰り返すことができる場合は、パフォーマンス パネルで録画してください。
遅延のすべて
以下の問題をすべて追加してみてください。
コード全体を見る: 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);
});
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 秒間のタスクがキューに入れられ、メインスレッドが長時間ブロックされます。
そのような時間のかかるタスクと新しいクリックが重なると、イベント リスナー自体がほぼ即座に結果を返しても、インタラクションが遅くなります。先ほどのテストと同じ状況で入力遅延を作成しました。今回のみ、入力遅延は 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);
});
パフォーマンス トレース
複数回クリックされても、blockFor
タスクは 1 つだけ実行され、クリックが 1 秒間発生しないと実行されます。テキスト入力や、複数のクイック クリックが予想されるアイテム ターゲットなど、突発的に発生するインタラクションには、デフォルトでこの戦略を使用するのが理想的です。
17. 方法 2: 長時間実行作業を中断する
残念なことに、デバウンス期間が過ぎた直後にさらにクリックが発生し、長いタスクの途中で発生し、入力遅延のためにインタラクションが非常に遅くなる可能性があります。
やり取りがタスクの途中で発生した場合は、忙しい作業を一時停止して、新しいやり取りがすぐに処理されるようにするのが理想的です。どのようにすればよいでしょうか。
isInputPending
のような API もありますが、長いタスクをチャンクに分割することをおすすめします。
多数の setTimeout
1 回目: 何か簡単なことを行います。
コード全体を見る: 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 クリックにつき 1 秒間のタスクを 10 個の 100 ミリ秒のタスクに分割しています。その結果、複数のインタラクションがこれらのタスクと重複する場合でも、どのインタラクションでも 100 ミリ秒を超える入力遅延は発生しません。ブラウザは、受信したイベント リスナーを setTimeout
の処理よりも優先し、インタラクションの応答性は保たれます。
この戦略は、独立したエントリ ポイントを多数スケジュールする場合に特に効果的です。たとえば、アプリケーションの読み込み時に呼び出す必要がある多数の独立した機能がある場合などです。デフォルトでは、スクリプトを読み込み、スクリプトの評価時にすべてを実行するだけで、巨大な長いタスクですべてが実行される可能性があります。
ただし、この戦略は、共有状態を使用する for
ループのように、密結合されたコードを分割する場合にはうまく機能しません。
yield()
で入手可能
ただし、最新の async
と await
を利用して「収益ポイント」を簡単に追加できます。渡すこともできます
例:
コード全体を見る: catchy.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 回目のクリックが発生すると、1 つ目のループが AbortController
でキャンセルされたと判断され、新しい blockInPiecesYieldyAborty
ループが開始されます。その後、最初のループが再び実行されるようにスケジュール設定すると、signal.aborted
が true
に変更され、それ以上の処理は行われず、すぐに結果に戻ります。
18. まとめ
長いタスクをすべて分割することで、サイトが新しい操作に対応できるようになります。これにより、最初のフィードバックを迅速に提供できるほか、進行中の作業を中止するなどの判断を下すことができます。場合によっては、エントリ ポイントを別のタスクとしてスケジュールする必要があります。場合によっては使用することもできます。
重要
- INP はすべてのインタラクションを測定します。
- 各インタラクションは、入力から次のペイント(ユーザーに見える応答性)まで測定されます。
- 入力遅延、イベント処理時間、プレゼンテーションの遅延はすべてインタラクションの応答性に影響します。
- DevTools を使用すると、INP とインタラクションの内訳を簡単に測定できます。
戦略
- ページに長時間実行コード(長時間のタスク)がない。
- 次のペイントまで、イベント リスナーから不要なコードを移動します。
- レンダリングの更新自体が、ブラウザで効率的に行われるようにします。