- 技術 💻
INP改善の方法|Core Web Vitalsの原因別コードと改善前後の実測値
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期連続の黒字決算。