はじめに
Googleがウェブサイトの表示体験を評価する指標 Core Web Vitals において、従来の FID(First Input Delay:初回入力遅延)に代わり、 INP(Interaction to Next Paint:次の描画までのインタラクション応答性) が正式な評価指標となりました。
FIDは「ユーザーが最初に行う操作の反応速度(最初の遅延のみ)」しか測定していませんでしたが、INPは「ユーザーがページに滞在している期間中のすべてのクリック、タップ、キーボード操作」を対象とし、操作してから実際に画面が書き換わるまでの遅延を測定します。
本記事では、INPの計測の仕組みと、JavaScriptの実行遅延によって発生するインタラクション障害を解消するための具体的なパフォーマンス改善手法を解説します。
1. INP(Interaction to Next Paint)の定義と仕組み
INPは、ユーザーがクリック、タップ、あるいはキーボード操作を行ってから、ブラウザが処理を完了し、「次のフレーム(Next Paint)」を画面に描画するまでの時間を測定した指標です。
INPの遅延は、主に以下の3つのフェーズの合計値で構成されています。
[インタラクション発生]
│
├─► 1. 入力遅延 (Input Delay)
│ メインスレッドで実行中の別タスクが完了するのを待つ時間
│
├─► 2. 処理時間 (Processing Time)
│ JavaScriptのイベントリスナー(onClickなど)が実行される時間
│
└─► 3. 描画遅延 (Presentation Delay)
ブラウザがレイアウトを再計算し、画面を再描画するまでの時間
│
[画面が更新される (Next Paint)]
ユーザーにとって「反応が速い」と感じる基準として、INPの値は 200ミリ秒以下 であることが推奨(Good評価)されています。
2. アプローチ1: ロングタスク(Long Tasks)の分割
INP低下の最大の原因は、JavaScriptの実行に 50ミリ秒以上 かかる「ロングタスク」がメインスレッドを占有し、ユーザーの操作イベントを割り込ませないことです。
重い処理を実行する必要がある場合は、処理を小さなタスクに分解し、ブラウザに制御を一時的に戻す(Yield)必要があります。
避けるべき実装(メインスレッドの占有)
function processHeavyData(items) {
// 巨大な配列を一括でループ処理するため、メインスレッドが数秒間フリーズする
for (const item of items) {
doHeavyCalculation(item);
}
}
改善された実装(Scheduler API / setTimeout によるタスクの分割)
モダンブラウザで推奨される scheduler.yield() またはフォールバックとしての setTimeout を使用し、ループの合間にブラウザの描画処理を挟み込ませます。
// ブラウザに実行権を譲るユーティリティ
const yieldToMain = () => {
if (globalThis.scheduler?.yield) {
return scheduler.yield();
}
return new Promise(resolve => setTimeout(resolve, 0));
};
async function processHeavyDataInChunks(items) {
let count = 0;
for (const item of items) {
doHeavyCalculation(item);
count++;
// 50回ごとにメインスレッドを開放し、ユーザー入力を受け付ける隙間を作る
if (count % 50 === 0) {
await yieldToMain();
}
}
}
3. アプローチ2: イベントハンドラーでの状態変更の最小化
ReactやVueなどのUIフレームワークにおいて、クリックイベント内で巨大な状態(State)更新を何個も同時に実行すると、仮想DOMの比較と再レンダリング処理が「処理時間(Processing Time)」を引き延ばします。
対策
- 状態更新のバッチ処理: 不要なレンダリングを引き起こさないよう、ローカルの状態更新をまとめるか、状態管理ツールを最適化します。
- 重い非同期処理の分離: サーバーへのAPIリクエスト(送信処理など)を行う際、ローカルのUI変更(ローディング表示など)をまず最優先で実行し、重い通信処理はイベントリスナーのコンテキストから非同期(バックグラウンド)で実行させます。
4. アプローチ3: 描画コストの削減(Presentation Delayの短縮)
JavaScriptのコード自体は早く終わっても、ブラウザが画面を描画するのに時間がかかっては意味がありません。
CSSトリガーによる再計算(リフロー)の最小化
JavaScript内でDOMの要素幅を読み取った直後に、その場で幅を書き換えるコードを書くと、ブラウザはレイアウトの再計算を強制されます。これを レイアウトスラッシング(Layout Thrashing) と呼びます。
- 悪い例:
const width = element.offsetWidth; // 読み取り element.style.width = (width + 10) + 'px'; // 書き込み (即座に再計算が発生) - 良い例:
読み取り処理(Read)と書き込み処理(Write)をグループ化して分離するか、
requestAnimationFrameを使用して描画フレームに同期させます。
CSSの content-visibility の活用
表示領域外(スクロールしないと見えない部分)にある重いDOM要素に対して、CSSの content-visibility: auto を適用することで、ブラウザは初期描画やレイアウト計算をスキップし、描画遅延を劇的に削減できます。
.heavy-footer-section {
content-visibility: auto;
contain-intrinsic-size: 500px; /* 事前に高さを予約してガタつきを防ぐ */
}
まとめ
INPの改善は、SEO(検索順位)の向上だけでなく、モバイルユーザーの直帰率を下げるためにも重要です。
- JavaScriptのロングタスク(50ms以上)を検知し、
yieldまたはsetTimeoutでタスクを分割する - クリックハンドラー内の処理を軽量化し、ローディング表示への切り替え速度を最速にする
- レイアウトスラッシングを防止し、
content-visibility等を活用してブラウザのレンダリング負担を減らす
これらのアプローチを駆使し、ストレスフリーで快適なレスポンスのWebサイトを目指しましょう。
