はじめに
2024年3月12日、Googleは新しいCore Web Vitalsの指標として INP(Interaction to Next Paint) を正式に導入し、従来の FID(First Input Delay) に代わる重要な指標となりました。
INPは、ユーザーがページ上で行うすべてのインタラクションの応答性を測定し、より実際のユーザー体験に近い評価を可能にします。本記事では、INPの仕組みから具体的な最適化手法まで、実践的な観点から解説します。
INPとは何か
INPの基本概念
INP(Interaction to Next Paint) は、ユーザーのインタラクション(クリック、タップ、キーボード入力)から、ブラウザが次のフレームを描画するまでの時間を測定する指標です。
FIDとINPの比較
項目 | FID(旧指標) | INP(新指標) |
---|---|---|
測定対象 | 最初のインタラクションのみ | すべてのインタラクション |
測定範囲 | 入力遅延のみ | 入力遅延 + 処理時間 + 描画遅延 |
評価方法 | 単一の値 | 最悪値(98パーセンタイル) |
実用性 | 限定的 | より実際のUXに近い |
INPの構成要素
INPは以下の3つの要素で構成されています:
要素 | 説明 | 主な影響要因 |
---|---|---|
入力遅延 | インタラクションからイベントハンドラー実行開始まで | メインスレッドのブロック |
処理時間 | イベントハンドラーの実行時間 | JavaScript処理の重さ |
描画遅延 | 処理完了から次のフレーム描画まで | DOMサイズ、CSS複雑さ |
INPの評価基準
評価 | 時間 | 状態 |
---|---|---|
🟢 良好 | ≤200ms | 優秀な応答性 |
🟡 改善必要 | 200-500ms | 改善の余地あり |
🔴 不良 | >500ms | 緊急対応が必要 |
INPが悪化する原因:シングルスレッドの特性
JavaScriptのシングルスレッド問題
JavaScriptは シングルスレッド 言語として設計されており、これがINP悪化の根本原因です。
メインスレッドの役割
主な問題とその影響
問題 | 説明 | INPへの影響 |
---|---|---|
ロングタスク | 50ms以上の処理 | 🔴 高(メインスレッドブロック) |
大きなDOM | 1400ノード超 | 🟡 中(レンダリング重い) |
重いJavaScript | 同期的な大量処理 | 🔴 高(処理時間増加) |
複雑なCSS | 複雑なセレクター | 🟡 中(描画遅延) |
問題の診断方法
// 簡単なINP測定コード
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'event' && entry.duration > 200) {
console.warn('遅いインタラクション検出:', {
要素: entry.target,
時間: entry.duration,
種類: entry.name
});
}
}
});
observer.observe({ entryTypes: ['event'] });
解決手法1:デバウンス処理
デバウンスとは
連続するイベントを制御し、最後のイベントから一定時間経過後に処理を実行する手法です。
効果的な使用場面
場面 | 効果 | 推奨遅延時間 |
---|---|---|
検索入力 | API呼び出し削減 | 300-500ms |
スクロール | 処理頻度制限 | 16ms(60FPS) |
リサイズ | レイアウト計算削減 | 250ms |
フォーム入力 | バリデーション最適化 | 500ms |
実装例(簡潔版)
// デバウンス関数
function debounce(func, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
}
// 使用例:検索入力
const searchInput = document.getElementById('search');
const debouncedSearch = debounce(performSearch, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
解決手法2:メインスレッド解放
タスク分割の基本戦略
手法 | 適用場面 | 実装難易度 |
---|---|---|
setTimeout(0) | 簡単な分割 | 🟢 易 |
requestIdleCallback | 空き時間活用 | 🟡 中 |
MessageChannel | 高精度制御 | 🔴 難 |
実装パターン
// パターン1: 簡単なタスク分割
async function processLargeData(data) {
const chunkSize = 100;
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
processChunk(chunk);
// メインスレッドに制御を戻す
await new Promise(resolve => setTimeout(resolve, 0));
}
}
// パターン2: 空き時間活用
function processInIdle(tasks) {
function processTasks() {
while (tasks.length > 0) {
const task = tasks.shift();
task();
// 時間切れチェック
if (performance.now() % 5 === 0) break;
}
if (tasks.length > 0) {
requestIdleCallback(processTasks);
}
}
requestIdleCallback(processTasks);
}
解決手法3:Web Worker活用
Web Workerの適用場面
処理タイプ | 適用度 | 理由 |
---|---|---|
数値計算 | 🟢 最適 | CPU集約的、DOM不要 |
データ変換 | 🟢 最適 | 大量データ処理 |
画像処理 | 🟢 最適 | 重い計算処理 |
DOM操作 | 🔴 不適 | Worker内でDOM不可 |
API呼び出し | 🟡 場合による | ネットワーク処理 |
基本的な実装パターン
// メインスレッド側
class SimpleWorker {
constructor(workerScript) {
this.worker = new Worker(workerScript);
}
async execute(data) {
return new Promise((resolve) => {
this.worker.onmessage = (e) => resolve(e.data);
this.worker.postMessage(data);
});
}
}
// 使用例
const worker = new SimpleWorker('calculator.js');
const result = await worker.execute({ numbers: [1,2,3,4,5] });
// Worker側(calculator.js)
self.onmessage = function(e) {
const { numbers } = e.data;
// 重い計算処理
const result = numbers.map(n => fibonacci(n));
self.postMessage(result);
};
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
最適化の優先順位
段階的アプローチ
段階 | 対策 | 効果 | 実装コスト |
---|---|---|---|
1. 基本 | デバウンス適用 | 🟡 中 | 🟢 低 |
2. 中級 | タスク分割 | 🟢 高 | 🟡 中 |
3. 上級 | Web Worker | 🟢 高 | 🔴 高 |
効果測定の指標
// 簡単なINP監視
class INPMonitor {
constructor() {
this.interactions = [];
this.setupObserver();
}
setupObserver() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'event') {
this.interactions.push({
duration: entry.duration,
type: entry.name,
timestamp: Date.now()
});
}
}
});
observer.observe({ entryTypes: ['event'] });
}
getINP() {
if (this.interactions.length === 0) return 0;
const durations = this.interactions.map(i => i.duration).sort((a, b) => a - b);
// 98パーセンタイルまたは最大値
return this.interactions.length < 50
? Math.max(...durations)
: durations[Math.floor(durations.length * 0.98)];
}
getReport() {
const inp = this.getINP();
const slowCount = this.interactions.filter(i => i.duration > 200).length;
return {
inp: Math.round(inp),
totalInteractions: this.interactions.length,
slowInteractions: slowCount,
status: inp <= 200 ? '良好' : inp <= 500 ? '改善必要' : '不良'
};
}
}
// 使用例
const monitor = new INPMonitor();
setInterval(() => {
console.log('INPレポート:', monitor.getReport());
}, 30000);
実践的なチェックリスト
即座に実行できる対策
- 検索・入力フィールドにデバウンス適用(300ms推奨)
- スクロールイベントにスロットリング適用(16ms推奨)
- 不要なイベントリスナーの削除
- DOMサイズの確認(1400ノード以下に)
- 長時間実行される処理の特定
中級者向け対策
- 重い処理のタスク分割実装
- requestIdleCallbackの活用
- 画像・動画の遅延読み込み
- CSSアニメーションの最適化
上級者向け対策
- Web Workerによる計算処理の分離
- SharedArrayBufferの活用(対応ブラウザのみ)
- Service Workerとの連携
- リアルタイム監視システムの構築
まとめ
INPの最適化は、段階的なアプローチ が重要です:
重要なポイント
- シングルスレッドの理解 - JavaScriptの特性を理解し、メインスレッドをブロックしない設計
- 段階的な最適化 - デバウンス → タスク分割 → Web Worker の順で実装
- 継続的な測定 - 最適化効果を定量的に測定し、継続的改善
- ユーザー中心の視点 - 技術指標だけでなく、実際のUX向上を最優先
今後の展望
Web技術の発展に伴い、INPの最適化手法も進化し続けています。新しいAPIや技術動向を常にキャッチアップし、ユーザーにとって最高の体験を提供することが重要です。
INPの最適化は一度限りの作業ではなく、継続的な改善プロセス です。定期的な測定と分析を通じて、常にユーザー体験の向上を目指しましょう。