2025年版 Webパフォーマンスの次なる一手!INPを200ms未満に引き下げる実践テクニック

Core Web Vitalsの新指標「INP(Interaction to Next Paint)」が、Webサイトのユーザー体験を決定づける新たな重要指標として注目を集めています。INPは、クリックやタップ、キー入力といったユーザーのインタラクションから、その結果として画面が更新されるまでの時間を計測します。GoogleはINPをCore Web Vitalsの一つとして位置づけ、75パーセンタイルで200ms未満を「良好」なユーザー体験の基準と定めています。最新のデータによると、INP単体の合格率は比較的高いものの、Core Web Vitals全体の3指標(LCP、CLS、INP)すべてを達成しているサイトは半数前後にとどまっており、今後ますますINP改善の重要性が高まっています。
本記事では、このINPを改善するための具体的なテクニック、特にブラウザのメインスレッドをブロックするLong Taskの分割、イベントの優先度設計、そして第三者スクリプトの隔離に焦点を当てて解説します。これらの手法を組み合わせることで、INPを目標値である200ms未満に引き下げるための現実的な解決策を探ります。
この記事でわかること
- INPを改善するための主要なテクニックとその実践方法
scheduler.yield()
やscheduler.postTask()
を使ったLong Taskの分割方法- イベントの優先度を適切に設計する重要性と具体的なアプローチ
- 第三者スクリプトがINPに与える影響とその対策
- Chrome DevToolsでINPの問題点を特定する実践的な方法
Long Taskを分割してINPを改善する
INPを悪化させる最大の原因の一つが、メインスレッドを長時間占有するLong Taskです。JavaScriptの実行時間が50msを超えるタスクはLong Taskと見なされます。このLong Task中にユーザーがインタラクションを行うと、処理が完了するまで画面の更新が遅延し、INPの値が大きくなります。例えば、フォーム送信ボタンを押した際、UIを即時更新せず、重いバリデーションやデータ送信処理を同期的に行ってしまうと、ユーザーは「何も反応しない」と感じてしまいます。
この問題を解決するには、一つの大きなタスクを複数の小さなタスクに分割し、その間にブラウザに制御を戻してレンダリングや他のイベント処理を可能にする必要があります。scheduler.yield()
とscheduler.postTask()
は、このタスク分割を効率的に行うための現代的なAPIです。
scheduler.yield()を使ったタスク分割
scheduler.yield()
は、現在のタスクの実行を一時停止し、ブラウザがレンダリングや他のイベント処理を行う機会を与えます。タスクを分割する際には、処理の区切りとなる部分でawait scheduler.yield()
を挿入します。
// Before: A long, blocking task
const blockingTask = async () => {
for (let i = 0; i < 10000; i++) {
// Some heavy computation
}
};
// After: Split into smaller tasks with scheduler.yield()
const nonBlockingTask = async () => {
for (let i = 0; i < 10000; i++) {
// Some heavy computation
if (i % 1000 === 0) {
// Yield control to the browser every 1000 iterations
await scheduler.yield();
}
}
};
バージョン依存性: scheduler.yield()
はまだすべてのブラウザでサポートされているわけではありません(2025年8月時点)。非対応ブラウザ向けには、requestAnimationFrame
やsetTimeout(0)
のような代替手段を用いるプログレッシブエンハンスメントが有効です。
const yieldToBrowser = () => {
// Use scheduler.yield() if available, otherwise fall back
return globalThis.scheduler?.yield?.() ?? new Promise(r => setTimeout(r, 0));
};
// Use it like: await yieldToBrowser();
- 既存の
setTimeout
/requestAnimationFrame
との違い:await Promise.resolve()
は同一タスクのマイクロタスクキューで継続されるため、描画の機会を与えません。一方、setTimeout(..., 0)
やrequestAnimationFrame
は新たなタスクとしてスケジュールされ、レンダリング前に制御を返します。scheduler.yield()
はこれらよりさらに効率的に、ブラウザがレンダリングやユーザー入力の処理を優先的に行えるよう制御を返します。
イベントの優先度設計の実践
INPを最適化するには、イベントハンドラー内の処理を適切に設計することが不可欠です。すべての処理を同期的に実行するのではなく、重要な処理とそうでない処理に分け、非同期に実行する工夫が必要です。現場での線引きとしては、UIの即時更新(例:ボタンのローディング状態への変更、フォームのクリア)は同期的に行い、ユーザーが待つ必要のない処理(例:分析データの送信、バックエンドAPIへのデータ送信)は非同期に切り出すのが鉄則です。
// Bad practice: All tasks are handled synchronously
document.getElementById('submit-btn').addEventListener('click', () => {
// Update UI (critical for INP)
updateUI();
// Send analytics data (not critical for INP)
sendAnalyticsData();
});
このコードでは、sendAnalyticsData()
が完了するまでUIの更新が遅れる可能性があります。以下のように、scheduler.postTask()
を使って優先度を分けることで、INPを改善できます。
// Good practice: Separate critical and non-critical tasks
const postBackground = (fn) =>
globalThis.scheduler?.postTask
? scheduler.postTask(fn, { priority: 'background' })
: setTimeout(fn, 0);
document.getElementById('submit-btn').addEventListener('click', () => {
// Update UI immediately (critical)
updateUI();
// Defer non-critical tasks to a background priority
postBackground(sendAnalyticsData);
});
- asyncイベントハンドラの活用:
async
イベントハンドラを使用し、await yieldToBrowser()
を挟むことで、一度制御を戻すテクニックも有効です。これはブラウザにUI更新の機会を与え、その後に非同期処理を続けることができます。
document.getElementById('submit-btn').addEventListener('click', async () => {
// UI更新など、高速な処理
updateUI();
// ここで描画&入力処理の機会を与える
await yieldToBrowser();
// 時間のかかる非同期処理を続ける
await heavyAsyncOperation();
});
第三者スクリプトの隔離と最適化
多くのWebサイトでは、広告、分析、SNSボタンなどの第三者スクリプトが読み込まれています。これらはメインスレッドをブロックし、INPを著しく悪化させる主要因です。
外部JSタグの最適化
async
/defer
属性: 外部スクリプトタグには、必ずasync
またはdefer
属性を付与しましょう。これにより、スクリプトのダウンロード中にHTMLパースがブロックされるのを防ぎます。partytown
のようなライブラリの活用: 広告や分析タグなど、Web Worker
化が難しい外部スクリプトの多くは、partytown
のようなライブラリを使ってメインスレッドから隔離できます。これにより、スクリプトがWeb Worker内で実行されるため、メインスレッドへの影響を最小限に抑えられます。- 実装時の落とし穴: なお、
Partytown
等の専用ソリューションをまず検討しましょう。自前の素のWorkerに第三者タグをそのまま移植するのは、DOM依存・CORS・同期APIの不在などの理由から実務上ハードルが高いです。
- 実装時の落とし穴: なお、
Web Workerによる隔離
Web Workerはメインスレッドとは独立したスレッドで動作するため、重い処理や長時間かかるスクリプトをメインスレッドから分離できます。
- Web Workerの制約: Web WorkerはDOMに直接アクセスできません。UIの更新が必要な場合は、
postMessage()
を使ってメインスレッドにデータを送信し、メインスレッド側でDOM操作を行う必要があります。また、CORSや外部通信の制約も考慮が必要です。
実践!INP問題の特定
INPの問題を特定するには、Chrome DevToolsのPerformanceパネルとLighthouseレポートが非常に役立ちます。
Performanceパネルでの診断
- DevToolsを開き、Performanceパネルに移動します。
- 記録ボタン(⚫︎)をクリックし、INPが気になるユーザー操作(例:ボタンクリック)を実行します。
- 記録を停止すると、タイムラインが表示されます。
- 「Main」セクションで、実行時間が50msを超えるLong Taskを探します。これらのタスクは赤い三角形で強調されます。
- さらに、「Interactions」という紫色のトラックに注目します。ここにはユーザーの操作イベントが記録されており、200msを超えるINPには赤い三角形が付くため、一目で問題のあるインタラクションを特定できます。
Main
スレッドのLong Taskをクリックすると、Summary
タブでCall Tree
やBottom-Up
ビューに切り替え、どの関数が実行時間の大部分を占めているかを特定できます。
Lighthouseレポートでの診断
- DevToolsのLighthouseパネルに移動します。
- 「Performance」のカテゴリにチェックを入れて、「Analyze page load」をクリックします。
- レポートの「Metrics」セクションでINPの値を確認します。
- 「Diagnostics」セクションには、INPを悪化させているLong Taskや第三者スクリプトに関する具体的な改善提案が表示されます。
よくある質問(FAQ)と現場の改善例
Q: INPだけ改善すればいい?LCPやFCPは無視しても大丈夫?
A: いいえ、Webサイトの全体的なユーザー体験を向上させるためには、Core Web Vitalsの各指標(LCP, CLS, INP)をバランスよく改善することが重要です。INPはインタラクションに対する応答性、LCPは初期表示の速さ、CLSは予期せぬレイアウトシフトを防ぐための指標であり、それぞれ異なる側面からユーザー体験を評価します。
Q: 既存のSPAフレームワーク(React/Next.js等)でも有効ですか?
A: はい、どんなフレームワークを使用していても、メインスレッドをブロックする重い処理や不要な同期化はINP悪化の原因になります。本記事で解説したタスク分割やイベントの優先度設計は、フレームワークに依存せず適用できる普遍的なテクニックです。
現場の改善例
とあるECサイトでは、商品詳細ページの読み込み時に大量の分析タグと広告タグが同期的に読み込まれており、INPが500msを超えることが課題でした。Partytown
を導入してこれらの第三者スクリプトをWeb Workerで実行するように変更したところ、メインスレッドが解放され、INPは170msまで大幅に改善しました。
まとめ
この記事では、INPを200ms未満に改善するための実践的なテクニックとして、scheduler.yield()
とscheduler.postTask()
を用いたLong Taskの分割、イベントの優先度設計、そして第三者スクリプトの隔離について解説しました。INPは、ユーザーのインタラクションに対する応答性を高め、Webサイトのユーザー体験を向上させる上で極めて重要な指標です。
これらのテクニックを駆使し、DevToolsを効果的に活用することで、あなたのサイトのINPを劇的に改善し、より快適なユーザー体験を提供できるはずです。この記事がINP改善に一歩踏み出すきっかけになれば嬉しいです!ご覧いただきありがとうございました。