INP(Interaction to Next Paint)完全攻略:シングルスレッドJavaScriptの特性と最適化手法

に公開

はじめに

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の最適化は、段階的なアプローチ が重要です:

重要なポイント

  1. シングルスレッドの理解 - JavaScriptの特性を理解し、メインスレッドをブロックしない設計
  2. 段階的な最適化 - デバウンス → タスク分割 → Web Worker の順で実装
  3. 継続的な測定 - 最適化効果を定量的に測定し、継続的改善
  4. ユーザー中心の視点 - 技術指標だけでなく、実際のUX向上を最優先

今後の展望

Web技術の発展に伴い、INPの最適化手法も進化し続けています。新しいAPIや技術動向を常にキャッチアップし、ユーザーにとって最高の体験を提供することが重要です。

INPの最適化は一度限りの作業ではなく、継続的な改善プロセス です。定期的な測定と分析を通じて、常にユーザー体験の向上を目指しましょう。

参考文献

Discussion