INP最適化 実践ガイド 2026年版 — 200ms以下のレスポンスを実現する方法

INPはCore Web Vitalsで最も不合格率が高い指標です。scheduler.yield()やWeb Workers、LoAF APIを使ってINPを200ms以下に改善する実践テクニックを、コード例とケーススタディを交えて解説します。

INP最適化ガイド2026 200ms以下を実現

はじめに:なぜ2026年にINP最適化が最重要課題なのか

FID(First Input Delay)がCore Web Vitalsから引退して、もう2年が経ちました。後継のINP(Interaction to Next Paint)が登場してから、Web開発者にとっての頭痛の種は確実に増えたんじゃないでしょうか。

2026年現在、全Webサイトの43%がINPの200ms閾値をクリアできていないというデータがあります。LCPやCLSと比べても、INPは断トツで不合格率が高い。正直、これはかなり深刻な数字です。

なぜこんなにも多くのサイトが苦戦しているのか?

答えはシンプルで、INPの改善はHTMLやCSSをちょっといじるだけでは済まないからです。JavaScriptのアーキテクチャそのものを見直す必要がある。FIDが「最初のインタラクションの入力遅延」だけを見ていたのに対し、INPはページのライフサイクル全体にわたるすべてのインタラクションを記録して、75パーセンタイルでの最悪値を報告します。つまり、ごまかしが効かないんですよね。

この記事では、2026年時点で使える最新API — scheduler.yield()scheduler.postTask()、Long Animation Frames API — と、現場で効果が実証されたテクニックを使って、INPを200ms以下に抑える方法を解説していきます。実際にINPを450msから48msに改善した事例や、売上7%増を達成した事例も紹介するので、ぜひ最後まで読んでみてください。

INPの3つのフェーズを理解する

INPを効果的に最適化するには、まずインタラクションが裏側でどう処理されているかを知っておく必要があります。INPは以下の3つのフェーズの合計時間で決まります。

1. 入力遅延(Input Delay)

ユーザーがクリックやタップをしてから、イベントハンドラの実行が始まるまでの待ち時間です。メインスレッドが長時間タスクで占有されていると、ユーザーのインタラクションはキューに入って処理待ちになります。実は90パーセンタイルでは、この入力遅延がINPの最大の原因になるケースが多いんです。

2. 処理時間(Processing Time)

イベントハンドラとコールバックの実行にかかる時間。平均してINP全体の約40%を占めるので、ここの最適化はインパクトが大きいです。

3. 描画遅延(Presentation Delay)

イベントハンドラの実行が終わってから、ブラウザが次のフレームをペイントするまでの時間。DOM要素が多いとスタイル計算やレイアウト処理が重くなって、ここで時間を食います。

この3フェーズのどこがボトルネックかによって、対策がまったく変わってきます。まずは測定から始めましょう。

INPの測定とデバッグ手法

web-vitalsライブラリでINPを計測する

Googleが提供するweb-vitalsライブラリを使えば、実際のユーザー環境でINPを手軽に計測して、分析データとして送信できます。アトリビューション付きビルドを使うと、どの要素のどのイベントがINPに影響したかまで特定できるのが嬉しいポイントです。

import { onINP } from 'web-vitals/attribution';

onINP((metric) => {
  console.log('INP value:', metric.value, 'ms');

  // アトリビューション情報で原因を特定
  const attribution = metric.attribution;
  console.log('イベントタイプ:', attribution.interactionType);
  console.log('対象要素:', attribution.interactionTarget);

  // 3フェーズの内訳
  console.log('入力遅延:', attribution.inputDelay, 'ms');
  console.log('処理時間:', attribution.processingDuration, 'ms');
  console.log('描画遅延:', attribution.presentationDelay, 'ms');

  // LoAFエントリとの関連付け
  if (attribution.longAnimationFrameEntries) {
    attribution.longAnimationFrameEntries.forEach((loaf) => {
      console.log('LoAFの持続時間:', loaf.duration, 'ms');
      loaf.scripts.forEach((script) => {
        console.log('原因スクリプト:', script.sourceURL);
        console.log('関数名:', script.sourceFunctionName);
        console.log('実行時間:', script.duration, 'ms');
      });
    });
  }

  // RUMサービスにデータを送信
  sendToAnalytics({
    name: 'INP',
    value: metric.value,
    id: metric.id,
    attribution: {
      type: attribution.interactionType,
      target: attribution.interactionTarget,
      inputDelay: attribution.inputDelay,
      processingDuration: attribution.processingDuration,
      presentationDelay: attribution.presentationDelay,
    },
  });
});

Long Animation Frames(LoAF)APIで詳細な原因を特定する

LoAF APIは、従来のLong Tasks APIを大幅に進化させたもので、Chrome 123から正式に使えるようになりました。Long Tasks APIが「長いタスクがあったよ」と教えてくれるだけだったのに対し、LoAFはどのスクリプトが、どの関数で、どれだけ時間を使ったかまで詳細にレポートしてくれます。

個人的に特に重要だと思うのは、個々のタスクが短くても、1つのアニメーションフレーム内に大量のタスクが集中すると全体としてフレームが遅延する — というLong Tasks APIでは見つけられなかったパターンもLoAFなら検出できる点です。これ、実際にデバッグしてみると結構あるんですよね。

// LoAF APIを使ったパフォーマンスモニタリング
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // 50ms以上かかったアニメーションフレームを検出
    if (entry.duration > 50) {
      console.log('Long Animation Frame検出');
      console.log('持続時間:', entry.duration, 'ms');
      console.log('ブロック時間:', entry.blockingDuration, 'ms');
      console.log('開始時刻:', entry.startTime);

      // 原因スクリプトの詳細情報
      entry.scripts.forEach((script) => {
        console.log('---スクリプト情報---');
        console.log('ソースURL:', script.sourceURL);
        console.log('関数名:', script.sourceFunctionName);
        console.log('呼び出し元:', script.invoker);
        console.log('呼び出しタイプ:', script.invokerType);
        console.log('実行開始:', script.executionStart);
        console.log('持続時間:', script.duration, 'ms');

        // 要素セレクターとイベントタイプの表示
        // 例:「BUTTON#submit.onclick」
        if (script.sourceCharPosition > 0) {
          console.log(
            'ソース位置:',
            `${script.sourceURL}:${script.sourceCharPosition}`
          );
        }
      });
    }
  }
});

observer.observe({ type: 'long-animation-frame', buffered: true });

LoAFのscriptsプロパティには、そのフレーム内で実行されたスクリプトの詳細情報が入っています。sourceURLsourceFunctionNameinvokerTypeevent-listenerresolve-promiseなど)といった情報から、正確なボトルネックを突き止められます。

ただし注意点が一つ。クロスオリジンのスクリプト(サードパーティタグなど)はsourceURLが空文字で返される場合があります。ブラウザの同一オリジンポリシーによるセキュリティ制限ですね。対策としては、サードパーティスクリプトのscriptタグにcrossorigin="anonymous"属性を追加してください。

scheduler.yield():メインスレッドへの協調的な譲歩

さて、ここからが本題です。INP最適化で今もっとも注目されているAPIがscheduler.yield()。Chrome 115のオリジントライアルを経て、現在はChromium系ブラウザ(Chrome、Edge)で正式に使えるようになっています。

なぜsetTimeoutではダメなのか

長年、メインスレッドへの譲歩にはsetTimeout(callback, 0)が使われてきました。でも、このアプローチには致命的な欠点があります。

setTimeoutで分割した後続タスクは、タスクキューの末尾に配置されるんです。つまり、他のタスク(サードパーティスクリプトやブラウザの内部処理)が先に実行されてしまう。結果として、本来の処理の再開が大幅に遅れることがあります。

scheduler.yield()はまさにこの問題を解決するために作られました。後続の処理をタスクキューの先頭に配置するので、ユーザーインタラクションの処理を優先しつつ、自分の処理もすぐに再開できます。この違い、地味に見えて実はめちゃくちゃ大きいです。

基本的な使い方

// scheduler.yield() の基本パターン
async function processLargeDataset(data) {
  const CHUNK_SIZE = 1000;

  for (let i = 0; i < data.length; i += CHUNK_SIZE) {
    // チャンクごとにデータを処理
    const chunk = data.slice(i, i + CHUNK_SIZE);
    processChunk(chunk);

    // メインスレッドに制御を返す
    // ブラウザはここでユーザーインタラクションを処理できる
    if ('scheduler' in window && 'yield' in scheduler) {
      await scheduler.yield();
    } else {
      // フォールバック:setTimeout(0)
      await new Promise((resolve) => setTimeout(resolve, 0));
    }
  }
}

// イベントハンドラ内での使用例
button.addEventListener('click', async () => {
  // Step 1: UIの即時フィードバック(最優先)
  showLoadingSpinner();

  // メインスレッドに譲歩 → ブラウザがUIを描画
  await scheduler.yield();

  // Step 2: データの取得と処理
  const data = await fetchData();
  processData(data);

  // 再度譲歩
  await scheduler.yield();

  // Step 3: UIの最終更新
  hideLoadingSpinner();
  renderResults(data);
});

scheduler.postTask()で優先度を制御する

scheduler.postTask()は、タスクに明示的な優先度を設定できるAPIです。3段階の優先度を使い分けることで、ブラウザに「どの処理を先にやるべきか」を伝えられます。

// scheduler.postTask() による優先度制御

// ユーザーのクリックに対する即時応答(最高優先度)
async function handleUserClick() {
  // UIフィードバックは最高優先度で即座に実行
  await scheduler.postTask(
    () => {
      updateButtonState('loading');
      showProgressIndicator();
    },
    { priority: 'user-blocking' }
  );

  // データ処理は通常優先度でスケジュール
  await scheduler.postTask(
    async () => {
      const results = await computeResults();
      renderResultsList(results);
    },
    { priority: 'user-visible' }
  );

  // アナリティクス送信は低優先度でバックグラウンド実行
  scheduler.postTask(
    () => {
      trackEvent('button_click', { timestamp: Date.now() });
      sendBeacon('/analytics', { event: 'click' });
    },
    { priority: 'background' }
  );
}

// TaskControllerでタスクをキャンセル可能にする
const controller = new TaskController({ priority: 'user-visible' });

scheduler.postTask(
  () => {
    performExpensiveComputation();
  },
  { signal: controller.signal }
);

// ユーザーが別のアクションを起こした場合、前のタスクをキャンセル
controller.abort();

TaskControllerを使えば、不要になったタスクをキャンセルすることもできます。たとえば検索入力欄でキーストロークごとに検索を走らせる場合、前回の検索タスクをキャンセルして最新の入力だけを処理する — というパターンにぴったりです。

Web Workersで重い処理をオフロードする

メインスレッドへの譲歩だけでは足りないケースもあります。暗号処理、画像リサイズ、大規模データのソートや集計など、本質的に重い計算処理はWeb Workersを使ってバックグラウンドスレッドに完全に移してしまうのが正解です。

基本的なWeb Worker実装

// worker.js — バックグラウンドスレッドで実行
self.addEventListener('message', async (event) => {
  const { type, payload } = event.data;

  switch (type) {
    case 'SORT_DATA': {
      // 大規模データのソート処理をバックグラウンドで実行
      const sorted = payload.data.sort((a, b) => {
        return a[payload.sortKey].localeCompare(b[payload.sortKey]);
      });
      self.postMessage({ type: 'SORT_COMPLETE', result: sorted });
      break;
    }

    case 'FILTER_AND_AGGREGATE': {
      // 複雑なフィルタリングと集計
      const filtered = payload.data.filter(payload.filterFn);
      const aggregated = filtered.reduce((acc, item) => {
        const key = item[payload.groupBy];
        if (!acc[key]) acc[key] = { count: 0, total: 0 };
        acc[key].count++;
        acc[key].total += item.value;
        return acc;
      }, {});
      self.postMessage({
        type: 'AGGREGATE_COMPLETE',
        result: aggregated,
      });
      break;
    }

    case 'HASH_PASSWORD': {
      // 暗号処理はメインスレッドで絶対にやってはいけない
      const encoder = new TextEncoder();
      const data = encoder.encode(payload.password + payload.salt);
      const hashBuffer = await crypto.subtle.digest('SHA-256', data);
      const hashArray = Array.from(new Uint8Array(hashBuffer));
      const hashHex = hashArray
        .map((b) => b.toString(16).padStart(2, '0'))
        .join('');
      self.postMessage({ type: 'HASH_COMPLETE', result: hashHex });
      break;
    }
  }
});

// main.js — メインスレッド側
class WorkerPool {
  constructor(workerScript, poolSize = navigator.hardwareConcurrency || 4) {
    this.workers = [];
    this.queue = [];
    this.available = [];

    for (let i = 0; i < poolSize; i++) {
      const worker = new Worker(workerScript);
      this.workers.push(worker);
      this.available.push(worker);
    }
  }

  execute(message) {
    return new Promise((resolve, reject) => {
      const worker = this.available.pop();

      if (worker) {
        worker.onmessage = (e) => {
          this.available.push(worker);
          this._processQueue();
          resolve(e.data);
        };
        worker.onerror = (e) => {
          this.available.push(worker);
          this._processQueue();
          reject(e);
        };
        worker.postMessage(message);
      } else {
        this.queue.push({ message, resolve, reject });
      }
    });
  }

  _processQueue() {
    if (this.queue.length > 0 && this.available.length > 0) {
      const { message, resolve, reject } = this.queue.shift();
      this.execute(message).then(resolve).catch(reject);
    }
  }

  terminate() {
    this.workers.forEach((w) => w.terminate());
  }
}

// 使用例
const pool = new WorkerPool('/worker.js', 4);

button.addEventListener('click', async () => {
  // UIは即座にフィードバック
  showSpinner();

  // 重い処理はWorkerで並列実行 → メインスレッドはブロックされない
  const result = await pool.execute({
    type: 'SORT_DATA',
    payload: { data: largeDataset, sortKey: 'name' },
  });

  hideSpinner();
  renderTable(result.result);
});

WorkerPoolパターンを使うと、CPUコア数に応じたワーカーを管理してタスクを効率的に分散できます。単にWorkerを1つ作るだけじゃなく、プールとして管理することで、複数の重い処理を同時に走らせても安定したパフォーマンスが得られます。

長いタスクの分割パターン

50msを超えるタスクは「長いタスク(Long Task)」とみなされ、INPの入力遅延フェーズに直接悪影響を与えます。ここでは、よくある長いタスクを分割するパターンをいくつか紹介します。

requestIdleCallbackで非重要処理を遅延実行する

// 非重要な処理をアイドル時間に実行する
function deferNonCriticalWork(tasks) {
  let taskIndex = 0;

  function processNext(deadline) {
    // ブラウザがアイドル状態の間、タスクを処理
    while (taskIndex < tasks.length && deadline.timeRemaining() > 5) {
      tasks[taskIndex]();
      taskIndex++;
    }

    // まだ残りタスクがあれば次のアイドル期間にスケジュール
    if (taskIndex < tasks.length) {
      requestIdleCallback(processNext, { timeout: 2000 });
    }
  }

  requestIdleCallback(processNext, { timeout: 2000 });
}

// 使用例:ページ読み込み後に実行する非重要タスク
deferNonCriticalWork([
  () => initAnalytics(),
  () => prefetchNextPageAssets(),
  () => loadChatWidget(),
  () => initA11yEnhancements(),
  () => precomputeSearchIndex(),
]);

イベントリスナーの最適化パターン

スクロールイベントは特に要注意です。何も対策しないと、毎秒何十回もイベントが発火してメインスレッドを圧迫します。

// 悪い例:スクロールイベントで重い処理を直接実行
window.addEventListener('scroll', () => {
  // レイアウトスラッシング(強制同期レイアウト)を引き起こす
  const rect = element.getBoundingClientRect();
  element.style.transform = `translateY(${rect.top}px)`;
  calculateExpensiveLayout();
  updateMultipleElements();
});

// 良い例:requestAnimationFrameとスロットリングで最適化
let ticking = false;

window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      // 読み取りと書き込みをバッチ処理
      const scrollY = window.scrollY;

      // スタイルの書き込みはまとめて1回
      updateElementPositions(scrollY);

      ticking = false;
    });
    ticking = true;
  }
}, { passive: true });

// passive: true を忘れないこと!
// スクロールイベントでpreventDefault()を呼ばないことを
// ブラウザに伝え、スクロールのブロックを防ぐ

DOM最適化でプレゼンテーション遅延を削減する

INPの第3フェーズ「描画遅延」は、DOMの大きさと複雑さに直結します。DOMが大きいほど、スタイル計算・レイアウト・ペイントのすべてが遅くなる。ここは意外と見落とされがちなポイントです。

CSS Containmentとcontent-visibilityの活用

/* CSS Containmentで再計算範囲を隔離 */
.product-card {
  contain: layout style paint;
  /* このカード内の変更は、カード外のレイアウトに影響しない */
}

/* content-visibility: auto で画面外要素のレンダリングをスキップ */
.feed-item {
  content-visibility: auto;
  contain-intrinsic-size: 0 200px;
  /* 画面外のアイテムはレンダリングをスキップ */
  /* ただし高さは200pxとして予約(CLSを防止) */
}

/* 長いリストに対してvirtualization的な効果を得る */
.long-list .list-item {
  content-visibility: auto;
  contain-intrinsic-size: auto 80px;
}

content-visibility: autoは、いわば仮想スクロール(virtualization)のCSS版です。画面外の要素のレンダリングを完全にスキップして、スクロールで画面内に入ったときだけレンダリングを行います。100件以上の商品リストやフィードで使うと、初期レンダリング時間がかなり短縮されます。正直、これだけでINPが劇的に改善するケースもあります。

サードパーティスクリプトの管理

INPの隠れた大敵がサードパーティスクリプトです。Google Tag Manager、ソーシャルメディアウィジェット、チャットツール、アナリティクスツール…知らないうちにメインスレッドを占有しているスクリプトって、意外と多いんですよね。

ファサードパターンによる遅延読み込み

// YouTube埋め込みのファサードパターン
class YouTubeFacade extends HTMLElement {
  connectedCallback() {
    const videoId = this.getAttribute('video-id');
    this.innerHTML = `
      <div class="youtube-facade"
           style="position:relative;cursor:pointer;
                  background:url(https://i.ytimg.com/vi/${videoId}/hqdefault.jpg)
                  center/cover no-repeat;
                  aspect-ratio:16/9;">
        <svg viewBox="0 0 68 48" width="68" height="48"
             style="position:absolute;top:50%;left:50%;
                    transform:translate(-50%,-50%)">
          <path d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19
                   C55.79.13 34 0 34 0S12.21.13 6.9 1.55
                   C3.97 2.33 2.27 4.81 1.48 7.74
                   .06 13.05 0 24 0 24s.06 10.95 1.48 16.26
                   c.78 2.93 2.49 5.41 5.42 6.19
                   C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55
                   c2.93-.78 4.64-3.26 5.42-6.19
                   C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z"
                fill="#f00"/>
          <path d="M45 24L27 14v20" fill="#fff"/>
        </svg>
      </div>
    `;

    // クリックされたときだけiframeを読み込む
    this.addEventListener('click', () => this._loadIframe(videoId), {
      once: true,
    });
  }

  _loadIframe(videoId) {
    this.innerHTML = `
      <iframe
        src="https://www.youtube.com/embed/${videoId}?autoplay=1"
        frameborder="0"
        allow="autoplay; encrypted-media"
        allowfullscreen
        style="width:100%;aspect-ratio:16/9;">
      </iframe>
    `;
  }
}

customElements.define('youtube-facade', YouTubeFacade);

ファサードパターンでは、重いサードパーティウィジェットの代わりに軽量なプレースホルダーを最初に表示して、ユーザーが実際に操作したときだけ本物を読み込みます。YouTubeの埋め込みだけでも数百KBのJavaScriptを節約でき、INPへの好影響は大きいです。

React / Next.jsでのINP最適化

ReactアプリケーションはINPの問題を抱えやすい傾向にあります。仮想DOM(VDOM)の差分計算、大規模なコンポーネントツリーの再レンダリング、肥大化したハイドレーション…。メインスレッドを圧迫する要因が構造的に多いんです。

useTransitionとuseDeferredValueの活用

import { useState, useTransition, useDeferredValue } from 'react';

function ProductSearch() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  // 重い検索結果のレンダリングを遅延させる
  const deferredQuery = useDeferredValue(query);

  function handleInput(e) {
    // 入力フィールドの更新は即座に反映(高優先度)
    setQuery(e.target.value);

    // 検索結果のフィルタリングは低優先度で遅延実行
    startTransition(() => {
      // この中の状態更新は中断可能
      // ユーザーの次の入力が来たら自動的に中断される
      filterProducts(e.target.value);
    });
  }

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleInput}
        placeholder="商品を検索..."
      />
      {isPending && <div className="loading-bar" />}
      {/* deferredQueryを使用して、結果の表示を遅延 */}
      <ProductList query={deferredQuery} />
    </div>
  );
}

// React.memoで不要な再レンダリングを防止
const ProductList = React.memo(function ProductList({ query }) {
  const filteredProducts = useMemo(
    () => products.filter((p) =>
      p.name.toLowerCase().includes(query.toLowerCase())
    ),
    [query]
  );

  return (
    <ul>
      {filteredProducts.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </ul>
  );
});

useTransitionはReact 18で導入された並行レンダリング機能で、低優先度の更新を「トランジション」として扱います。ユーザーのキーボード入力のような高優先度のインタラクションが来たら、トランジション中のレンダリングを中断して先にそちらを処理してくれる。これがINPの改善に直結します。

実際のケーススタディ

理論だけじゃ説得力に欠けるので、実際にINPを劇的に改善した事例を見てみましょう。

事例1:ECサイトでINPを450msから48msに改善

あるECサイトでは、Speculation Rules APIを導入して、ユーザーが商品カードにホバーした時点で次のページをプリレンダリングする仕組みを実装しました。結果は驚くべきものでした:

  • INP: 450ms → 48ms(89%削減)
  • 直帰率: 18%低下
  • Google Ads CPC: 12%低下(品質スコア改善による)

89%削減って、ちょっと信じがたい数字ですよね。でも、プリレンダリングによってページ遷移時のメインスレッド負荷がほぼゼロになるので、理にかなっています。

事例2:RedBus — 売上7%増

Googleの公式ケーススタディによると、オンラインバスチケットプラットフォームのRedBusは、検索ページでの日付変更インタラクションの最適化とイベントリスナーの整理によってINPを改善。結果として売上が7%増加しました。パフォーマンス改善が直接的にビジネスの数字を動かした好例です。

事例3:REI — ReactアプリでINP 50%改善

アウトドアブランドのREIは、Long Tasksの分割とトラッキングイベントの遅延実行で、モバイルユーザーのINPを50%以上改善しました。CrUXデータでもしっかり改善が確認されています。

これらの事例に共通するのは、「メインスレッドの負荷を減らす」というシンプルな原則。具体的な手法は違っても、根本のアプローチは同じです。

INP最適化チェックリスト

最後に、すぐに使えるINP最適化チェックリストをまとめておきます。優先度の高いものから順に並べているので、上から順番に取り組んでみてください。

  1. 測定する:web-vitalsライブラリとLoAF APIで現状を把握する
  2. 長いタスクを分割する:50ms以上のタスクをscheduler.yield()で分割する
  3. 重い処理をWeb Workersに移す:ソート、暗号化、データ変換など
  4. サードパーティスクリプトを監査する:不要なものは削除、必要なものはファサードパターンで遅延読み込み
  5. DOMサイズを縮小するcontent-visibility: autoの活用、不要な要素の削除
  6. イベントリスナーを最適化するpassive: trueの設定、スロットリングの実装
  7. Reactアプリの場合useTransitionuseDeferredValueReact.memoの活用
  8. CSS Containmentを適用するcontainプロパティでレイアウト再計算の範囲を制限
  9. アナリティクスは低優先度で送信するscheduler.postTask()background優先度を使用
  10. 継続的にモニタリングする:CrUXデータで28日間のトレンドを追跡する

よくある質問(FAQ)

INPとFIDの違いは何ですか?

FID(First Input Delay)はユーザーの最初のインタラクションの入力遅延のみを測定していました。一方でINP(Interaction to Next Paint)は、ページのライフサイクル全体にわたるすべてのインタラクション(クリック、タップ、キー入力)を記録し、入力遅延・処理時間・描画遅延の3フェーズを含めた総合的なレスポンス速度を測定します。75パーセンタイルで報告されるので、実際のユーザー体験をより正確に反映しているといえます。

INPのスコアが悪いとSEOに影響しますか?

はい、影響します。INPはCore Web Vitalsの一つで、Googleの検索ランキング要因の一部です。Core Web Vitals3指標すべてをクリアしたサイトは直帰率が約24%低下するという調査結果もあります。ただし、コンテンツの質やその他のSEO要因も当然重要なので、INPだけでランキングが劇的に変わるわけではありません。バランスが大事です。

scheduler.yield()はすべてのブラウザで使えますか?

scheduler.yield()は現在、Chromium系ブラウザ(Chrome、Edge)で利用可能です。FirefoxやSafariではまだサポートされていないので、if ('scheduler' in window && 'yield' in scheduler)で機能検出を行い、非対応ブラウザではsetTimeout(resolve, 0)をフォールバックとして使ってください。npmで公式ポリフィルも入手できます。

モバイルでINPが特に悪いのはなぜですか?

モバイルデバイスのCPU性能はデスクトップの3〜5倍遅いとされています。それにタッチ操作特有のレイテンシやメモリ制約も加わるので、デスクトップと比べて60〜80%悪いINPスコアが出ることも珍しくありません。モバイルファーストでINP最適化に取り組むことをおすすめします。

INPを改善するために最も効果的な方法は?

まずはweb-vitalsライブラリとLoAF APIで「どのインタラクションが遅いのか」を特定するのが最優先です。闇雲に最適化しても効果は薄いので。その上で、長いJavaScriptタスクの分割(scheduler.yield()の使用)、不要なサードパーティスクリプトの削除、Web Workersによる重い処理のオフロードが、最も効果的な3つの施策です。個人的な経験では、この3つだけでINPを200ms以下にできるケースがほとんどでした。

著者について Daniel Okafor

Daniel started in performance work on the SRE side. He spent six years at Spotify on the Web Player team, where he owned the TTI regression budget for the desktop web app and built the internal dashboard that flagged perf regressions per PR before merge. He left in 2023 to join a small consultancy doing performance audits for fintech and travel companies, mostly in the UK and Nigeria. His subspecialty is server-side rendering tradeoffs: when streaming SSR actually helps, when it makes things worse on flaky 4G, and the real numbers behind React Server Components for content-heavy sites. He's a heavy Playwright user for perf testing, mistrusts most npm dependencies on principle, and is currently writing a small Rust tool to diff WebPageTest waterfalls across deploys. Outside of work he coaches a junior dev meetup in Manchester.