INP(Interaction to Next Paint)とは、クリック・タップ・キー入力から「画面が次に描き変わるまで」の時間を測る、Core Web Vitalsの応答性の指標です。75パーセンタイルで200ミリ秒以下なら「良好」とされます。改善の基本は、遅れの原因が「入力遅延」「処理時間」「表示遅延」のどこにあるかを切り分け、その区分に合った直し方を選ぶことです。

この記事では、INPの仕組みと基準値を公式情報(web.dev)で確認したうえで、よくある4つの原因をローカルのテストページで再現し、Playwright(Chromium 141)とweb-vitalsライブラリ 6.2.2で改善前後のINPを実測しました。修正コードはそのまま貼って試せる形で載せています。「content-visibilityを1件ずつに付けたら逆に遅くなった」という意外な結果も、数値ごと紹介します。

INP改善の早見表

遅れている区分 よくある原因 主な改善方法 この記事の実測(改善前→後)
処理時間(Processing duration) クリック時の重い計算・大量のDOM更新 先に見た目を更新し、残りを scheduler.yield() で分割 792ms → 16ms
処理時間+表示遅延 1文字入力ごとの全件フィルタ・全件描画 デバウンス+表示件数の上限 216ms → 24ms
表示遅延(Presentation delay) 大きなDOMのスタイル再計算・レイアウト 描画量を減らす(ページ分割・仮想化)、content-visibility をまとまり単位で 960ms → 40〜56ms
入力遅延(Input delay) 裏で動く長いタスク(タイマー・サードパーティ) 長いタスクを50ms未満に分割、読み込みの遅延・削減 160ms → 16ms
共通 どこが遅いかわからない web-vitalsの attribution ビルドで3区分を記録 ―

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「INP改善の方法|Core Web Vitalsの原因別コードと改善前後の実測値」

INPの仕組みと基準値

基準値は200ミリ秒と500ミリ秒

web.devの公式解説(2025年9月更新)によると、INPはページ上のインタラクションのうち最も遅いものに近い値で、評価は実ユーザーの75パーセンタイルで行います。

評価 INP 同じ枠組みのLCP CLS
良好(Good) 200ms以下 2.5秒以下 0.1以下
改善が必要 200ms超〜500ms以下 2.5秒超〜4秒以下 0.1超〜0.25以下
不良(Poor) 500ms超 4秒超 0.25超
  • 対象の操作:マウスクリック、タップ、キー入力。スクロールやホバーは対象外です。
  • 外れ値の扱い:インタラクションが多いページでは、50回ごとに最も遅い1回を除外します。操作が50回未満なら、最も遅い1回がそのままINPになります。
  • FIDとの関係:INPは2024年3月にFID(First Input Delay)に代わってCore Web Vitalsになりました。FIDは「最初の1回の入力遅延だけ」でしたが、INPはページ滞在中のすべての操作と、処理と描画までの時間を含みます。

1回のインタラクションは3つの区分に分かれる

flowchart LR
    A[ユーザーがクリック] -->|入力遅延<br>Input delay| B[イベントハンドラ開始]
    B -->|処理時間<br>Processing duration| C[ハンドラ終了]
    C -->|表示遅延<br>Presentation delay| D[次のフレームを描画]
区分 何の時間か 長くなる典型例
入力遅延 入力を受けてからハンドラが動き出すまで メインスレッドが別の長いタスクで埋まっている
処理時間 pointerdown・click・input などのハンドラの実行 ハンドラ内の重い計算、同期的なDOM更新
表示遅延 ハンドラ終了後、スタイル計算・レイアウト・描画を経て画面に出るまで DOMが大きい、変更の影響範囲が広い

3区分の合計がINPの1回分です。どの区分が長いかで打ち手が変わるため、まず計測で切り分けます。

計測方法の一覧

ツール データの種類 わかること 使いどころ
PageSpeed Insights フィールド(CrUX)+ラボ 実ユーザーのINP(28日間)、ラボではTBT まず現状をつかむ
Search Console(ウェブに関する主な指標) フィールド(CrUX) 「INPの問題」があるURLグループ サイト全体で悪いページ群を探す
CrUX(BigQuery / CrUX API / CrUX Vis) フィールド オリジン・URL単位の推移 月次の傾向を見る
Chrome DevTools(Performanceパネル) ラボ 操作ごとのINP、3区分、長いタスクの中身 原因の関数を特定する
web-vitals(npmライブラリ) 自サイトの実ユーザー INP値+3区分+対象要素 自前で集計して原因ページを特定

Lighthouse単体ではINPは測れません(操作がないため)。ラボではTBT(Total Blocking Time)が目安になります。実ユーザーのINPを測るには、フィールドデータかweb-vitalsを使います。

web-vitalsでINPと3区分を記録する

npmの web-vitals 6.2.2(2026年9月時点の最新)で、attributionビルドを使うと3区分と対象要素がわかります。

npm install web-vitals
// main.js(バンドラ利用時)
import { onINP } from 'web-vitals/attribution';

onINP((metric) => {
  const a = metric.attribution;
  const body = JSON.stringify({
    value: Math.round(metric.value),          // INP(ms)
    rating: metric.rating,                    // 'good' | 'needs-improvement' | 'poor'
    target: a.interactionTarget,              // 例: '#calc'
    type: a.interactionType,                  // 'pointer' | 'keyboard'
    inputDelay: Math.round(a.inputDelay),
    processing: Math.round(a.processingDuration),
    presentation: Math.round(a.presentationDelay),
    url: location.pathname,
  });
  navigator.sendBeacon('/analytics/inp', body); // 自前の収集エンドポイント
});

ポイント:onINP は既定ではページが非表示になったとき(タブ切替・遷移)に1回報告します。開発中に毎回見たいときは第2引数に { reportAllChanges: true } を渡します。longAnimationFrameEntries には、そのインタラクションに重なった長いアニメーションフレーム(LoAF)の情報が入り、どのスクリプトが時間を使ったかをたどれます。

検証環境と計測条件

以下の実測は、すべてこのマシンで行った値です。絶対値は端末で大きく変わるため、改善前後の差に注目してください。

項目 内容
ブラウザ Chromium 141.0.7390.37(Playwright 1.56.1、ヘッドレス)
計測ライブラリ web-vitals 6.2.2(attribution ビルド、reportAllChanges: true)
CPU Intel Xeon 2.10GHz・2コア、CDPで CPUスロットリング 4倍(中位のスマホ相当を想定)
方法 各パターン5回計測し、中央値を採用。操作は page.mouse.click と page.keyboard.type
注意 Event Timing APIの仕様で、値は8ms単位に丸められます

計測用スクリプトの骨格です(テストページは各セクションのコードをまとめた1ファイル)。

// measure.mjs(Node.js 22 / playwright 1.56.1)
import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage();
const cdp = await page.context().newCDPSession(page);
await cdp.send('Emulation.setCPUThrottlingRate', { rate: 4 }); // CPUを4倍遅く
await page.goto('http://127.0.0.1:7250/page.html?s=A&m=before');

// page.click は内部でページ内のチェック処理を待つため、長いタスクの隙間でしか押せない。
// 入力遅延を正しく再現するため、座標を指定して mouse.click を使う
const b = await page.locator('#calc').boundingBox();
await page.mouse.click(b.x + b.width / 2, b.y + b.height / 2);
await page.waitForTimeout(3000);

console.log(await page.evaluate(() => window.__inp.at(-1))); // onINPで貯めた最新値
await browser.close();

ハマりどころ:最初は page.click() で計測したところ、裏で長いタスクが走るパターンでも入力遅延がほぼ0msになりました。page.click() は要素のチェックをページ内で実行してからクリックするため、長いタスクが終わった直後にしか押されないからです。座標指定の page.mouse.click() に変えると、入力遅延が再現しました。

原因1:クリック時の重い処理(処理時間が長い)

改善前:ハンドラの中で全部計算してから表示する

// 60万までの素数を数える(重い計算の例)
function countPrimes(from, to) {
  let c = 0;
  for (let n = from; n < to; n++) {
    let p = n > 1;
    for (let d = 2; d * d <= n; d++) { if (n % d === 0) { p = false; break; } }
    if (p) c++;
  }
  return c;
}

document.getElementById('calc').addEventListener('click', () => {
  const total = countPrimes(0, 600000);            // ここが長いタスク
  document.getElementById('status').textContent = `結果: ${total}`;
});

改善後:先に見た目を変え、残りを小分けにして譲る

// scheduler.yield() があれば使い、なければ setTimeout で代用
function yieldToMain() {
  if (globalThis.scheduler?.yield) return scheduler.yield();
  return new Promise((resolve) => setTimeout(resolve, 0));
}

document.getElementById('calc').addEventListener('click', async () => {
  const status = document.getElementById('status');
  status.textContent = '集計中…';                   // 1. すぐに反応を見せる
  await yieldToMain();                              // 2. 描画の機会を渡す
  let total = 0;
  const STEP = 50000;
  for (let i = 0; i < 600000; i += STEP) {
    total += countPrimes(i, Math.min(i + STEP, 600000));
    await yieldToMain();                            // 3. 小分けにして都度譲る
  }
  status.textContent = `結果: ${total}`;
});

実測結果

パターン INP(中央値) 入力遅延 処理時間 表示遅延 5回の値
改善前 792ms(不良) 8ms 777ms 7ms 320, 336, 792, 808, 992
改善後 16ms(良好) 5ms 0ms 12ms 8, 16, 16, 16, 16

INPは「次の描画まで」を測るため、最初の描画を先に済ませれば、残りの計算がどれだけ長くてもそのインタラクションのINPには入りません。ただし、分割しないまま後ろに回すと、次の操作の入力遅延になります。だから「先に表示」と「小分け」をセットにします。

scheduler.yield() と setTimeout の違いと対応状況

方法 再開の順番 対応状況(MDN browser-compat-data 8.1.3で確認)
scheduler.yield() 続きの処理を優先的に再開(他の待ちタスクに割り込まれにくい) Chrome/Edge 129以降、Firefox 142以降。Safariは未対応(Baseline外)
setTimeout(fn, 0) タスクキューの最後尾に回る 全ブラウザ
requestAnimationFrame → setTimeout 次の描画の直後に実行 全ブラウザ

Safariでも動かすため、上の yieldToMain() のように機能検出してフォールバックする書き方が安全です。

原因2:1文字ごとの全件フィルタと全件描画

改善前

const items = Array.from({ length: 5000 }, (_, i) =>
  `備品-${String(i).padStart(4, '0')} ${['机', '椅子', 'モニター', 'ケーブル', '棚'][i % 5]}`);
const list = document.getElementById('list');

function renderList(keyword, max) {
  const hit = items.filter((t) => t.includes(keyword));
  const shown = max ? hit.slice(0, max) : hit;
  list.innerHTML = shown.map((t) => `<div class="card">${t}</div>`).join('');
}

document.getElementById('q').addEventListener('input', (e) => {
  renderList(e.target.value, 0);   // 1文字ごとに全件を描き直す
});

改善後:デバウンス+表示は50件まで

let timer;
document.getElementById('q').addEventListener('input', (e) => {
  const kw = e.target.value;
  clearTimeout(timer);
  timer = setTimeout(() => renderList(kw, 50), 200); // 入力が200ms止まってから、50件だけ描画
});

デバウンス(debounce)とは、連続するイベントが一定時間止まってから1回だけ処理する手法です。入力欄の文字自体はブラウザがすぐに描画するので、打鍵の反応は速いまま保てます。

パターン(「1234」を120ms間隔で入力) INP(中央値) 入力遅延 処理時間 表示遅延
改善前 216ms(改善が必要) 4ms 133ms 79ms
改善後 24ms(良好) 13ms 0ms 11ms

処理時間だけでなく表示遅延も縮んでいる点に注目してください。描画する要素数を減らすことが、DOM更新後のレイアウト時間の短縮にも効いています。

原因3:大きなDOMと広い範囲のスタイル変更(表示遅延が長い)

2万件のカードを並べたページで、body のクラスを切り替えてテーマを変える操作を測りました。

.card { padding: 8px; margin: 4px; border: 1px solid #ccc; border-radius: 6px; }
body.dark .card { background: #222; color: #eee; border-color: #555; }
document.getElementById('theme').addEventListener('click', () => {
  document.body.classList.toggle('dark'); // 処理は一瞬。重いのは後のスタイル計算と描画
});

4つの方法を比べた結果です。

パターン INP(中央値) 入力遅延 処理時間 表示遅延
改善前(2万件そのまま) 960ms 8ms 5ms 947ms
各 .card に content-visibility: auto 2,352ms(悪化) 96ms 374ms 1,882ms
100件ずつの section に content-visibility: auto 56ms 2ms 6ms 49ms
200件だけ描画(ページ分割) 40ms 0ms 9ms 31ms

content-visibilityは「まとまり」に付ける

/* 100件ずつ <section class="group"> で包み、そのまとまりに付ける */
.group {
  content-visibility: auto;              /* 画面外のまとまりは描画処理を省く */
  contain-intrinsic-size: auto 6000px;   /* 省いている間の仮の高さ */
}

content-visibility: auto は、画面外の要素のレイアウトと描画を省略するCSSプロパティです(Chrome 85、Firefox 125、Safari 18以降)。今回の計測では、2万個の小さな要素それぞれに付けると、要素ごとの管理コストが上回って逆に遅くなりました。100件ずつのまとまりに付けると、表示遅延は947ms→49msに縮みました。付ける単位で結果が大きく変わるので、必ず改善前後を計測して確かめるのがおすすめです。

DOMを減らすのが最も確実

  • ページ分割・「もっと見る」・仮想スクロールで、画面にある要素を数百件程度に保つ
  • スタイル変更の影響範囲を狭くする(body ではなく対象コンテナのクラスを切り替える)
  • アニメーションは transform と opacity を使い、レイアウトを起こすプロパティ(width・top など)を避ける
  • 読み取り(offsetHeight 等)と書き込み(style 変更)を交互に行わない(強制同期レイアウトを避ける)

Lighthouseは、DOM要素数が多いページで「過大な DOM サイズの回避」の診断を表示します。目安として確認しておくと原因の当たりがつけやすくなります。

原因4:裏で動く長いタスク(入力遅延が長い)

広告・チャット・計測タグなどのサードパーティスクリプトや、自前のポーリング処理が長いタスクを作ると、ユーザーの操作がその終了を待たされます。200msごとに150msの処理が走るページで、軽いクリック処理を10回計測しました。

// 改善前:150msを一気に実行
setInterval(() => {
  const end = performance.now() + 150;
  while (performance.now() < end) {} // 重い処理の代わり
}, 200);

// 改善後:30ms×5に分割し、合間でメインスレッドを譲る
setInterval(async () => {
  for (let k = 0; k < 5; k++) {
    const end = performance.now() + 30;
    while (performance.now() < end) {}
    await yieldToMain();
  }
}, 200);
パターン INP(中央値) 入力遅延 処理時間 表示遅延
改善前(150msのタスク) 160ms 147ms 5ms 8ms
改善後(30ms×5に分割) 16ms 1ms 0ms 14ms

ハンドラ自体は数ミリ秒で終わるのに、INPは160msでした。自分のコードに問題がなくても、他の処理で遅くなる例です。50msを超えるタスクは「長いタスク(Long Task)」と呼ばれ、分割の目安になります。

サードパーティスクリプトの整理チェックリスト

確認項目 対応例
使っていないタグが残っていないか タグマネージャーの棚卸しをして停止
読み込み方 <script async> / defer、操作後や表示後の遅延読み込み
チャット・動画などの埋め込み クリックされるまで軽いプレースホルダー(ファサード)を表示
同じ機能のタグが重複していないか 計測タグを1つにまとめる
重い処理をメインスレッド外へ Web Worker に計算を移す

改善の進め方(手順)

flowchart TD
    A[Search Console / PSIで<br>INPが悪いURLを特定] --> B[web-vitals attributionで<br>対象要素と3区分を収集]
    B --> C{一番長い区分は?}
    C -->|入力遅延| D[裏の長いタスクを分割<br>サードパーティを整理]
    C -->|処理時間| E[先に見た目を更新<br>scheduler.yieldで分割<br>デバウンス]
    C -->|表示遅延| F[DOMを減らす<br>変更範囲を狭める<br>content-visibility]
    D --> G[DevToolsで改善前後を計測]
    E --> G
    F --> G
    G --> H[28日後にCrUXで確認]

フィールドデータ(CrUX)は28日間の集計なので、修正の効果が反映されるまで時間がかかります。修正直後はDevToolsやweb-vitalsの自前計測で確認し、フィールドデータは推移で見ます。

よくある失敗と直し方

よくある失敗 起きること こうするとよい
async 関数にしただけ await がなければ処理は分割されない await yieldToMain() を明示的に入れる
重い処理を後ろに回しただけ 次の操作の入力遅延になる 後ろに回した処理も小分けにする
content-visibility を小さな要素すべてに付ける 今回の計測では2.4倍悪化 まとまり単位に付けて計測で確認
Lighthouseのスコアだけ見る INPは測られない TBTを目安にしつつ、フィールドデータとDevToolsで確認
ラボで速い端末だけ試す 実ユーザーとの差が大きい DevToolsのCPUスロットリング(4倍・6倍)で試す

LCP・CLSとの関係(簡単に)

指標 測るもの 主な改善策
LCP 最大の要素(ヒーロー画像など)が表示されるまで 画像の最適化、fetchpriority="high"、サーバー応答の短縮
CLS レイアウトのずれ 画像・広告枠に width/height や aspect-ratio を指定
INP 操作への応答 この記事の内容

3指標とも75パーセンタイルで「良好」を満たすと、そのページはCore Web Vitalsに合格です。INPの改善で大きなDOMを減らすと、LCPやメモリ使用量にも良い影響が出ることがよくあります。

よくある質問

Q. INPはSEOの順位にどのくらい影響しますか? A. Core Web VitalsはGoogleのページエクスペリエンスの一部として評価に使われますが、検索順位は主にコンテンツの関連性で決まります。INPの改善は、順位対策というより「使いやすさと離脱の減少」への投資と考えるのが現実的です。

Q. PageSpeed Insightsで「データがありません」と出ます。 A. CrUXに十分な実ユーザーのデータが集まっていないページです。その場合は、web-vitalsで自前計測するか、DevToolsのPerformanceパネルで操作を記録して確認します。

Q. React や Vue のアプリでも同じ考え方ですか? A. はい。3区分で切り分ける考え方は同じです。Reactなら useTransition や useDeferredValue で重い更新の優先度を下げる、リストを仮想化する、といった手段が処理時間と表示遅延の短縮に使えます。

Q. scheduler.yield() はSafariで使えますか? A. 2026年9月時点のMDNの互換性データでは、Safariは未対応です。機能検出して setTimeout にフォールバックする yieldToMain() の形で書けば、全ブラウザで動きます。

まとめ

  • INPは操作から次の描画までの時間で、75パーセンタイル200ms以下が良好
  • 入力遅延・処理時間・表示遅延の3区分で切り分け、web-vitalsのattributionで記録する
  • 重い処理は先に見た目を更新し、scheduler.yield() で小分けに(実測792ms→16ms)
  • 入力欄はデバウンス+表示件数の上限(実測216ms→24ms)
  • 大きなDOMは描画量を減らす。content-visibility はまとまり単位で(実測960ms→56ms)
  • 裏の長いタスクも分割すると入力遅延が縮む(実測160ms→16ms)
  • 改善は必ず前後を計測し、フィールドデータは28日の推移で確認する

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「INP改善の方法|Core Web Vitalsの原因別コードと改善前後の実測値」

株式会社RJCでは、計測して原因を切り分け、改善前後を数字で確かめる進め方を、プロジェクトの中で仲間と共有しています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?