次のペイントに対するインタラクションを最適化する

ウェブサイトの Interaction to Next Paint を最適化する方法を学習します。

Interaction to Next Paint(INP)は、安定した Core Web Vitals 指標であり、ユーザーによるページ訪問の有効期間全体を通じて発生するすべての条件を満たすインタラクションのレイテンシをモニタリングして、ユーザー インタラクションに対するページの全体的な応答性を評価します。最終的な INP 値は、観測された最長のインタラクションです(外れ値は無視される場合もあります)。

優れたユーザー エクスペリエンスを提供するため、ウェブサイトは、Interaction to Next Paint を 200 ミリ秒以下にするよう努力する必要があります。ほとんどのユーザーでこの目標値を達成するには、ページ読み込みの 75 パーセンタイルをモバイル デバイスとデスクトップ デバイスでセグメント化して測定することをおすすめします。

適切な INP 値は 200 ミリ秒以下、低い値は 500 ミリ秒を超え、その間はすべて改善が必要です。

ウェブサイトによっては、インタラクションがほとんどないか、まったくないことがあります。たとえば、ほとんどがテキストと画像で構成され、インタラクティブな要素がほとんどないページなどです。また、テキスト エディタやゲームなどのウェブサイトでは、数百から数千ものインタラクションが発生することもあります。どちらの場合も、INP が高い場合、ユーザー エクスペリエンスが危険にさらされます。

INP を改善するには時間と労力が必要ですが、その報酬はユーザー エクスペリエンスの向上につながります。このガイドでは、INP を改善する方法について詳しく説明します。

INP が低い原因を把握する

遅いインタラクションを修正するには、ウェブサイトの INP が低いか、改善が必要かを示すデータが必要です。これらの情報が得られたら、ラボに移り、遅いインタラクションの診断を開始し、解決策を導き出します。

フィールド内で遅いインタラクションを見つける

INP の最適化のプロセスは、フィールド データから始めるのが理想的です。最良の場合、Real User Monitoring(RUM)プロバイダからのフィールド データは、ページの INP 値だけでなく、INP 値自体の原因となった特定のインタラクション、ページ読み込み中または読み込み後にインタラクションが発生したかどうか、インタラクションの種類(クリック、キー押下、タップ)などの貴重な情報を示すコンテキスト データも提供します。

フィールド データの取得に RUM プロバイダを利用していない場合は、INP フィールド データガイドPageSpeed Insights から Chrome ユーザー エクスペリエンス レポート(CrUX)を使用して不足を補うことをおすすめします。CrUX は Core Web Vitals プログラムの公式データセットであり、INP を含む何百万ものウェブサイトに関する指標の概要を提供します。しかし、CrUX では多くの場合、問題の分析に役立つコンテキスト データは RUM プロバイダから取得されません。そのため、可能な限り RUM プロバイダを使用するか、独自の RUM ソリューションを実装して CrUX で利用できる機能を補完することをおすすめします。

ラボでの遅いインタラクションを診断する

インタラクションが遅いことを示すフィールド データが得られたら、ラボでテストを開始するのが理想的です。フィールド データがない場合、ラボでの遅いインタラクションを特定するには、いくつかの戦略があります。たとえば、一般的なユーザーフローに沿って操作をテストしたり、メインスレッドが最もビジー状態になることが多い読み込み中の操作をテストしたりして、ユーザー エクスペリエンスの重要な部分で遅い操作を明らかにできるようにします。

インタラクションを最適化する

遅いインタラクションを特定し、ラボで手動で再現できるら、次のステップとして最適化を行います。お客様とのやり取りは、次の 3 つのフェーズに分けられます。

  1. 入力遅延: ユーザーがページで操作を開始したときに開始し、操作に対応するイベント コールバックの実行を開始したときに終了します。
  2. 処理時間。イベントのコールバックが完了するまでにかかる時間です。
  3. 表示遅延。インタラクションの視覚結果を含む次のフレームがブラウザに表示されるまでにかかる時間です。

この 3 つのフェーズの合計が、インタラクション レイテンシの合計です。インタラクションの各フェーズは、全体的なインタラクション レイテンシにある程度の時間に関わるため、インタラクションの各部分を最適化する方法を把握して、できるだけ短い時間で実行することが重要です。

入力遅延を特定して短縮する

ユーザーがページでなんらかの操作を行った場合、その最初の部分は入力遅延です。ページ上の他のアクティビティによっては、入力遅延がかなり長くなる場合があります。これは、メインスレッドで発生したアクティビティ(スクリプトの読み込み、解析、コンパイルなど)、フェッチ処理、タイマー関数、あるいは連続して互いに重複する他のインタラクションが原因で発生する可能性があります。

インタラクションの入力遅延の原因が何であれ、入力遅延を最小限にして、インタラクションでできるだけ早くイベント コールバックの実行を開始できるようにする必要があります。

起動時のスクリプト評価と長時間タスクの関係

ページのライフサイクルにおけるインタラクティビティの重要な側面は、起動時です。ページが読み込まれると、最初にレンダリングされますが、ページがレンダリングされたからといって、必ずしも読み込みが完了したとは限りません。ページを完全に機能させるために必要なリソースの数によっては、読み込み中にユーザーがページを操作しようとする可能性があります。

ページの読み込み中にインタラクションの入力遅延を長くする要因の 1 つは、スクリプトの評価です。ネットワークから JavaScript ファイルを取得しても、ブラウザは JavaScript を実行できるようになります。そのためには、スクリプトを解析して構文が有効であることを確認し、バイトコードにコンパイルし、最後に実行します。

スクリプトのサイズによっては、この作業でメインスレッドに長いタスクが発生し、ブラウザが他のユーザー操作に応答するのが遅れる可能性があります。ページの読み込み中にユーザー入力に対するページの応答性を維持するには、ページの読み込み中に長時間のタスクが発生する可能性を減らし、ページの応答性を維持する方法を理解することが重要です。

イベント コールバックを最適化する

入力遅延は、INP が測定する最初の部分にすぎません。また、ユーザー操作に応じて実行されるイベント コールバックが、できるだけ早く完了できるようにする必要があります。

頻繁にメインスレッドに譲る

イベント コールバックを最適化する際は、できる限り処理を少なくすることをおすすめします。しかし、操作ロジックが複雑になり、実行する処理をわずかに減らすことができる場合もあります。

ご自身のウェブサイトでこれに該当する場合は、次に試してみましょう。イベント コールバックの作業を別々のタスクに分割することです。これにより、作業全体の処理がメインスレッドをブロックする長いタスクになることを防ぎ、メインスレッドで待機している他のインタラクションをより早く実行できるようになります。

渡されたコールバックは新しいタスクで実行されるため、setTimeout はタスクを分割する方法の一つです。setTimeout を単独で使用するか、人間工学的に見直しのために、その使用を別の関数に抽象化できます。

無差別に譲歩するよりも、まったく譲らないほうがよいでしょう。ただし、メインスレッドに譲歩するより微妙な方法があります。また、ユーザー インターフェースを更新するイベント コールバックの直後にのみ譲歩することで、レンダリング ロジックを早く実行できます。

レンダリング処理を早く行えるようにする

より高度な yield の手法では、イベント コールバックでコードを構造化して、実行する内容を次のフレームのビジュアル更新を適用するために必要なロジックのみに制限します。それ以外はすべて、次のタスクに延期できます。これにより、コールバックが軽くて機敏に保たれるだけでなく、イベントのコールバック コードで視覚的な更新をブロックできないため、インタラクションのレンダリング時間が短縮されます。

たとえば、入力に応じてテキストの書式を設定するだけでなく、入力内容に応じて UI の他の要素も更新するリッチテキスト エディタを考えてみます(単語カウント、スペルミスのハイライト表示、その他の重要な視覚的フィードバックなど)。さらに、アプリを離れて戻ったときに作業内容が失われないように、アプリケーションで記述内容を保存する必要がある場合もあります。

この例では、ユーザーが入力した文字に応じて、次の 4 つのことを行う必要があります。ただし、次のフレームが表示される前に完了する必要があるのは、最初のアイテムのみです。

  1. ユーザーが入力した内容でテキスト ボックスを更新し、必要な書式を適用します。
  2. 現在の単語カウントを表示する UI の部分を更新します。
  3. スペルミスをチェックするためのロジックを実行します。
  4. 最新の変更を(ローカルまたはリモートのデータベースに)保存します。

これを行うコードは、次のようになります。

textBox.addEventListener('input', (inputEvent) => {
  // Update the UI immediately, so the changes the user made
  // are visible as soon as the next frame is presented.
  updateTextBox(inputEvent);

  // Use `setTimeout` to defer all other work until at least the next
  // frame by queuing a task in a `requestAnimationFrame()` callback.
  requestAnimationFrame(() => {
    setTimeout(() => {
      const text = textBox.textContent;
      updateWordCount(text);
      checkSpelling(text);
      saveChanges(text);
    }, 0);
  });
});

以下の図は、重要でない更新を次のフレームまで延期することで、処理時間の短縮、ひいては全体的な操作レイテンシの短縮につながることを示しています。

2 つのシナリオにおけるキーボード操作とその後のタスクの図。上の図では、レンダリング クリティカルなタスクと後続のすべてのバックグラウンド タスクが、フレームを表示する機会になるまで同期的に実行されます。下の図では、レンダリング クリティカルな処理が最初に実行され、次にメインスレッドに譲歩して新しいフレームをより早く提示します。その後、バックグラウンド タスクが実行されます。
上の図をクリックすると、高解像度バージョンが表示されます。

前のコード例では、requestAnimationFrame() 呼び出し内で setTimeout() を使用するのはやや難解ですが、重要でないコードが次のフレームをブロックしないようにするには、すべてのブラウザで有効な方法です。

レイアウト スラッシングを回避する

レイアウト スラッシング(強制同期レイアウトと呼ばれることもあります)は、レイアウトが同期的に発生するレンダリング パフォーマンスの問題です。これは、JavaScript でスタイルを更新し、それを同じタスクで読み取るときに発生します。また、レイアウト スラッシングを引き起こす可能性がある JavaScript のプロパティが多数ある場合です。

Chrome DevTools のパフォーマンス パネルに表示されたレイアウト スラッシングの可視化。
Chrome DevTools のパフォーマンス パネルに表示されたレイアウト スラッシングの例。レイアウト スラッシングを伴うレンダリング タスクには、コールスタックの該当部分の右上に赤い三角形が表示されています。多くの場合、Recalculate Style または Layout というラベルが付いています。

レイアウトのスラッシングはパフォーマンスのボトルネックです。スタイルを更新してからすぐにそのスタイルの値を JavaScript でリクエストすると、ブラウザは同期レイアウト処理を行わなければならなくなり、イベント コールバックの実行が完了した後に非同期の実行を待機していた可能性があるためです。

プレゼンテーションの遅延を最小化

インタラクション マークの表示遅延は、インタラクションのイベント コールバックの実行が完了してから、ブラウザで視覚的な変化を示す次のフレームをペイントできるようになるまでの時間です。

DOM サイズを最小限に抑える

ページの DOM が小さい場合、レンダリング処理は通常すぐに終了します。ただし、DOM のサイズが非常に大きくなると、DOM のサイズの増加に伴ってレンダリング処理がスケーリングされる傾向があります。レンダリング処理と DOM サイズの関係は直線的なものではありませんが、大きな DOM は小さな DOM よりもレンダリングに多くの作業を必要とします。大きな DOM では、次の 2 つのケースで問題が発生します。

  1. 最初のページ レンダリングでは、大きな DOM でページの初期状態のレンダリングに多くの作業が必要になります。
  2. ユーザー操作に応じて、大きな DOM のためにレンダリングの更新に非常にコストがかかるため、ブラウザが次のフレームを表示するまでの時間が長くなります。

大きな DOM を大幅に減らすことができない場合があることに留意してください。DOM サイズを縮小するには、DOM をフラット化する、最初の DOM サイズを小さく保つためにユーザー操作中に DOM を追加するなど、いくつかの手法がありますが、これらの手法は限界です。

content-visibility を使用して画面外の要素を遅延レンダリングする

ページの読み込み時のレンダリング処理と、ユーザーの操作に応じたレンダリング処理の両方を制限する方法の一つは、CSS の content-visibility プロパティを使用することです。これは、ビューポートに近づくときに要素のレンダリングを遅延する量になります。content-visibility を効果的に使用するには練習が必要ですが、その結果としてページの INP を改善できるレンダリング時間が短くなったかどうかを調べる価値はあります。

JavaScript を使用して HTML をレンダリングする場合のパフォーマンス コストに注意する

HTML がある場合、HTML 解析が行われます。ブラウザは HTML の解析を DOM に終えた後、スタイルを適用し、レイアウト計算を実行してから、そのレイアウトをレンダリングする必要があります。避けられないコストですが、HTML をレンダリングする方法が重要です。

サーバーが HTML を送信すると、ブラウザにストリームとして到着します。ストリーミングとは、サーバーからの HTML レスポンスがチャンク形式で到着することを意味します。ブラウザは、ストリームの到着時にチャンクを段階的に解析し、少しずつレンダリングすることで、ストリームの処理方法を最適化します。これはパフォーマンスの最適化です。ページ読み込み時にブラウザが定期的かつ自動的に暗黙的に譲歩し、それを無料で得ることができます。

ウェブサイトへの初回アクセスには常にある程度の HTML が関与しますが、一般的なアプローチは、最初のわずかな HTML から始まり、JavaScript を使用してコンテンツ領域に入力されます。その後のコンテンツ領域の更新も、ユーザーの操作によって行われます。これは通常、シングルページ アプリケーション(SPA)モデルと呼ばれます。このパターンの欠点の 1 つは、クライアントで JavaScript を使用して HTML をレンダリングすると、その HTML を作成するための JavaScript 処理のコストが発生するだけでなく、その HTML の解析とレンダリングが完了するまでブラウザ側では処理を実行できないことです。

ただし、SPA ではないウェブサイトであっても、インタラクションの結果として JavaScript によってある程度の HTML レンダリングが必要になる可能性が高いことを覚えておいてください。クライアント側で大量の HTML をレンダリングするのでない限り、通常は問題ありません。レンダリングによって次のフレームの表示が遅れる可能性があります。ただし、ブラウザで HTML をレンダリングするこのアプローチがパフォーマンスに及ぼす影響と、JavaScript を使用して多数の HTML をレンダリングする場合にウェブサイトのユーザー入力に対する応答性にどのような影響を及ぼすかを理解しておくことは重要です。

おわりに

サイトの INP の改善は反復的なプロセスです。現場での遅いインタラクションを修正すると、特にウェブサイトで多くのインタラクションが発生すると、遅いインタラクションが他にも見つかる可能性が高くなり、最適化も必要になります。

INP を改善する鍵は、永続性です。いずれは、ユーザーが提供しているエクスペリエンスに満足する場所に合わせてページの応答性を向上できるようになります。また、ユーザーのために新機能を開発するときに、ユーザーに合わせてインタラクションを最適化する際も同じプロセスを経る必要がある可能性もあります。時間と労力はかかりますが、時間と労力を振り返りましょう。

ヒーロー画像(作成者: David PisnoyUnsplash より)。Unsplash ライセンスに沿って修正。