INP中心の画像配信最適化 2025 — decode/priority/スクリプト協調で体感を守る
公開: 2025年9月21日 · 読了目安: 4 分 · 著者: Unified Image Tools 編集部
はじめに
LCP 改善は画像最適化の中心課題ですが、2024–2025 の評価では INP(Interaction to Next Paint)悪化が見落とされがちです。重い画像のデコードやメインスレッドを占有する初期化処理は、初回操作後の応答を鈍らせます。特に、ユーザーが最初にタップ/スクロールした直後に「遅延読み込みの噴出」「画像デコード」「スクリプト初期化」が重なると、フレーム欠落や操作遅延が増幅されます。
本稿では「レイアウトからの過配信排除」を土台に、デコード/優先度制御/遅延とスクリプト協調の観点で、INP を悪化させない画像配信の実務を整理します。Next.js(App Router)/ブラウザ API の使い分け、ヒーローと下位画像の役割分担、そして実地計測(RUM)までをひとつのフレームワークにまとめます。
TL;DR
- まず過配信を断つ(レイアウト由来の
sizes/srcset
設計) - LCP 候補のみ
priority
/fetchPriority="high"
、非LCPはdecoding="async"
と遅延 - 初回操作の前後で「画像読み込み/デコード/スクリプト」を競合させない(協調)
- プレースホルダーは CLS を出さない方式で(LQIP/BlurHash 等)
詳しいリサイズと sizes/srcset
は 2025年のレスポンシブ画像設計 — srcset/sizes 実践ガイド と 2025年のリサイズ設計 — レイアウトから逆算して 30–70% の無駄を削る を参照。
なぜ画像が INP を悪化させるのか(簡易メンタルモデル)
ブラウザは「ネットワーク取得 → デコード → レイアウト/描画」という段階を踏みます。INP は「入力→次のペイント」までの遅延ですから、入力処理の前後に重い画像デコードや再レイアウトが走ると悪化します。
- 入力直後に LazyLoad が大量発火すると、メインスレッドが decode/layout で埋まりイベント処理が遅延
- サイズ過大な画像はデコード時間が長い(過配信は LCP/INP の両方を悪化)
- スクリプトの初期化(パララックス、フィルタ、Canvas 処理)とデコードが衝突
回避策は「適正サイズ」「優先度の選別」「タイミング分離(協調)」の三位一体です。
INP を悪化させる典型パターン
- 過大サイズ画像のデコードが入力処理と同時に発生
- 初回操作に反応して、画像の LazyLoad が一斉に起動(スクロール/タップ直後のフレーム欠落)
- 画像初期化(Canvas/フィルタ/アニメーション)がメインスレッドを塞ぐ
これらは「重さ」そのものよりも、タイミング競合が根本原因です。
アンチパターンと対策早見表
- スクロール開始と同時に offscreen 画像を
eager
化 → rootMargin を活用し、200–400px 手前で段階ロード - 低速端末でもヒーロー以外に
priority
乱用 → LCP 候補のみ strict 運用(後述チェックリスト) - CSS アニメーション/Canvas と同時に大量 decode → 画像は idle/scheduler で時間をずらす
Next.js(App Router)での実装の型
// Hero: LCP 候補(初期表示)
<Image
src="/hero-1536.avif"
alt="製品ヒーロー"
fill
sizes="(max-width: 768px) 100vw, 768px"
priority
fetchPriority="high"
decoding="sync"
/>
// 下位折り返し: 非LCP(視界に入る直前に)
<Image
src="/gallery-640.webp"
alt="ギャラリー"
width={640}
height={360}
sizes="(max-width: 768px) 100vw, 768px"
loading="lazy"
decoding="async"
/>
ポイント:
- LCP 候補のみ sync/priority/high。その他は lazy/async でメインスレッドを空ける
sizes
が正しければ、ブラウザの選択は最適化される(過配信が INP/LCP を同時に悪化)
追加のヒント(リソースヒント/接続最適化)
// <head> でのヒント例(App Router: layout.tsx など)
export const metadata = {
other: {
link: [
{ rel: 'preconnect', href: 'https://cdn.example.com', crossOrigin: 'anonymous' },
],
},
};
- 画像 CDN へ preconnect すると往復遅延を短縮(ただし乱用は逆効果)
priority
と組み合わせるのは LCP 候補のみ(ヒーロー/ファーストビューの代表画像)
遅延の粒度とスクリプト協調
- ユーザー入力前後 300–500ms は重い decode/初期化を避ける
- IntersectionObserver で「視界の手前」で事前ロードし、操作直後の競合を回避
- 長い初期化(Canvas/フィルタ)は
requestIdleCallback
とスケジューリング
const io = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) {
// すぐ描画が必要なものだけを優先
e.target.setAttribute('loading', 'eager');
io.unobserve(e.target);
}
}
}, { rootMargin: '200px 0px' });
さらに、入力後すぐの 300ms で重い処理を避ける簡易スケジューラを併用します。
let lastInput = 0;
['pointerdown','keydown','wheel','touchstart'].forEach((t) => {
window.addEventListener(t, () => (lastInput = performance.now()), { passive: true });
});
export function scheduleAfterInput<T>(task: () => T) {
const dt = performance.now() - lastInput;
if (dt < 300) {
// 入力直後は少し遅らせる
setTimeout(task, 300 - dt);
} else {
requestIdleCallback(() => task());
}
}
プレースホルダーと CLS
CLS を出さない寸法指定(width/height or aspect-ratio)と、軽量な LQIP/SQIP/BlurHash を併用します。実装は プレースホルダー設計 LQIP/SQIP/BlurHash の実践 2025 を参照。
なお、ヒーローに過度なブラーを乗せると INP の改善に寄与しないことが多いです。プレースホルダーは「シンプルで軽い」が原則です。
計測とガードレール
- field INP を RUM で収集(web-vitals)
- 操作直後の Long Task を監視し、画像 decode/初期化の発火タイミングを見直す
- Lighthouse/Timespan で操作シナリオを採点
RUM(実ユーザー計測)の例(簡略)
import { onINP, onLCP } from 'web-vitals/attribution';
onINP((metric) => {
// 入力直後の競合を疑うとき、メタ情報を一緒に送る
fetch('/rum', {
method: 'POST',
keepalive: true,
body: JSON.stringify({
name: metric.name,
value: metric.value,
entries: metric.entries?.length,
}),
headers: { 'content-type': 'application/json' },
});
});
onLCP((metric) => {
// LCP と画像優先度の相関も記録
});
手動検証(Timespan)
-
シナリオ: ページ表示 → 0.5s 後にスクロール → 画像が視界に入る直前
-
DevTools の Performance/Long Task を確認、デコードが操作と衝突していないかをチェック
-
DevTools の Performance/Long Task を確認、デコードが操作と衝突していないかをチェック
ケーススタディ(現場シナリオ)
事例1: LP のヒーロー + 折り返し直下ギャラリー
- 症状: 初回スクロール直後に 20 枚のサムネイルが一斉に読み込み/デコード、INP p75 が 280→360ms に悪化
- 原因:
loading="lazy"
の root 視界判定が厳密で、スクロール開始直後に大量ヒット。sizes
が過小で過配信も発生 - 対策:
sizes
をレイアウト実寸に合わせて修正(過配信 35% 削減)rootMargin: '300px 0px'
で先読みを段階的に分散requestIdleCallback
でサムネイル初期化(ぼかし解除/アニメーション)を入力直後 300ms 以降に移動
- 結果: INP p75 を 360→250ms に改善、LCP も 3% 向上
事例2: EC 商品リスト(フィルタ UI + 画像切り替え)
- 症状: フィルタ押下後、画像差し替えと Canvas 加工が衝突して応答鈍化
- 原因: 同期デコード + 即時 Canvas 描画でメインスレッド占有
- 対策:
- 画像は
decoding="async"
に統一、差し替えは fade-in で視覚許容 - Canvas 加工は web worker へ逃がす or
OffscreenCanvas
- 重要でない加工はユーザー入力 300ms 以降に遅延
- 画像は
- 結果: 操作直後のフレーム欠落が解消、INP p75 が 280→210ms
実装パターン(競合を避ける)
1) decode タイミングの制御
// 画像を生成しておき、idle で decode を前倒し
export function predecodeOnIdle(src: string) {
const img = new Image();
img.src = src;
requestIdleCallback(async () => {
try { await img.decode(); } catch {}
});
}
注意: 大量の事前 decode は逆効果。可視直前の 1–3 枚に限定。
2) scheduler.postTask で優先度分離(対応ブラウザのみ)
// @ts-ignore: 型ガードはプロダクションで厳密に
const schedulerAny: any = (globalThis as any).scheduler;
export async function afterInputLow(task: () => void) {
// 入力直後は低優先度へ
const dt = performance.now() - (window as any).__lastInputTs ?? 0;
if (dt < 300 && schedulerAny?.postTask) {
await schedulerAny.postTask(task, { priority: 'background', delay: 300 - dt });
} else {
requestIdleCallback(task);
}
}
イベントフック:
['pointerdown','keydown','wheel','touchstart'].forEach((t) => {
addEventListener(t, () => ((window as any).__lastInputTs = performance.now()), { passive: true });
});
3) IntersectionObserver の段階ロード
const io = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) {
const el = e.target as HTMLImageElement;
// 見えた直前は eager、遠いものは lazy のまま
el.loading = 'eager';
io.unobserve(el);
}
}
}, { rootMargin: '300px 0px' });
export function watch(img: HTMLImageElement) { io.observe(img); }
4) ネットワーク条件で rootMargin を切替
function chooseRootMargin() {
const n = (navigator as any).connection;
if (!n) return '200px 0px';
if (n.saveData) return '150px 0px';
if (n.effectiveType?.includes('2g')) return '400px 0px';
return '300px 0px';
}
Next.js での落とし込み(App Router)
サーバ/クライアントの役割分担
- サーバ: 正しい
sizes
をレイアウトから算出して埋め込む - クライアント: 入力イベント観測と初期化タスクの抑制(300ms 窓)
- 画像 CDN の最適化はプリセット化し、
src
は指紋付き URL に固定
ヒーローとギャラリーのテンプレート化
// components/HeroImage.tsx
export function HeroImage(props: Omit<React.ComponentProps<'img'>, 'decoding'|'loading'>) {
return (
<img {...props} decoding="sync" fetchPriority="high" />
);
}
// components/GalleryImage.tsx
export function GalleryImage(props: Omit<React.ComponentProps<'img'>, 'decoding'>) {
return (
<img {...props} loading="lazy" decoding="async" />
);
}
プロジェクト全体で HeroImage
/GalleryImage
を利用し、lint で直書きの priority
を禁止します。
監視(Long Task / Event Timing)
// Long Task を観測
new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
const lt = e as PerformanceEntry & { duration: number };
if (lt.duration > 50) {
// 入力直後の 300ms 窓に重なっていないかを確認
}
}
}).observe({ type: 'longtask', buffered: true });
// Event Timing (INP の素地)
new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
// e.name (click, keydown など) / processingStart / duration
}
}).observe({ type: 'event', buffered: true });
RUM では p50/p75/p95 を分けて保存し、デプロイごとのリグレッションを検出します。ユーザーエージェント/ネットワーク種別別に切ると再現が取りやすくなります。
FAQ(よくある質問)
Q. priority
を複数の画像に付けてもいい?
A. 原則 NG。ヒーロー 1 枚(またはカルーセル 1 枚目)に限定。複数付与はネットワーク飽和と decode 競合の温床。
Q. decoding="sync"
はいつ使う?
A. LCP 候補で、視覚的に初期表示の一部である画像のみ。その他は async
。
Q. 画像 CDN への preconnect
は効果ある?
A. ファーストビューで CDN を叩くなら有効。ただしリンク数が多いと相殺されるため、1–2 ドメインに限定。
Q. Blur プレースホルダーは INP に効く?
A. 直接はほぼ効かない。CLS を避ける/主観的な読み込み感を出すための補助と捉える。
ガイドライン(チェックリスト拡張)
- [ ] レイアウトに一致する
sizes
を全画像に付与(過配信ゼロを目標) - [ ] LCP 候補のみ
priority
/fetchPriority="high"
/decoding="sync"
- [ ] 非 LCP は
loading="lazy"
+decoding="async"
- [ ] 入力直後 300–500ms 窓に重い decode/初期化を置かない
- [ ] rootMargin で段階ロード(200–400px 目安、接続速度で調整)
- [ ] RUM で INP/LCP 相関を記録、p75 を主指標に
- [ ] Lint/CI で
priority
の乱用・sizes
欠落を検出
まとめ
INP を守る鍵は「過配信を断つ」「優先度を正しく」「操作直前後に重い処理を置かない」の3点です。LCP 施策に加えて、タイミング協調の設計を加えるだけで、体感は大きく改善します。仕上げに RUM で回帰を監視し、ヒーロー以外の priority
を禁止するルール(lint/CI)を運用へ組み込みましょう。
チェックリスト(配信用):
- [ ] LCP 候補のみ
priority
/fetchPriority="high"
- [ ] 非 LCP は
loading="lazy"
+decoding="async"
- [ ]
sizes
はレイアウトと一致(過配信なし) - [ ] rootMargin で段階ロード、入力直後 300ms は重処理なし
- [ ] width/height or aspect-ratio 指定で CLS=0 を目指す
関連記事
画像最適化の基本 2025 — 勘に頼らない土台づくり
どのサイトにも効く、速くて美しい配信のための最新ベーシック。リサイズ→圧縮→レスポンシブ→キャッシュの順で安定運用に。
画像SEO 2025 — alt・構造化データ・サイトマップの実務
検索流入を逃さない画像SEOの最新実装。altテキスト/ファイル名/構造化データ/画像サイトマップ/LCP最適化を一つの方針で整えます。
画像圧縮 完全戦略 2025 ─ 画質を守りつつ体感速度を最適化する実践ガイド
Core Web Vitals と実運用に効く最新の画像圧縮戦略を、用途別の具体的プリセット・コード・ワークフローで徹底解説。JPEG/PNG/WebP/AVIF の使い分け、ビルド/配信最適化、トラブル診断まで網羅。
アクセシビリティ実務の画像 — alt/装飾/図解の線引き 2025
スクリーンリーダーで破綻しない画像の実装。装飾は空alt、意味画像は簡潔に、図解は本文を要約。リンク画像とOGPの注意点も。
Favicon & PWA アセット チェックリスト 2025 — マニフェスト/アイコン/SEO シグナル
見落としがちなファビコン/PWA アセットの要点。マニフェストのローカライズや配線、必要サイズの網羅をチェックリスト化。
フォーマット変換の戦略 2025 — WebP/AVIF/JPEG/PNG を使い分ける指針
コンテンツ種別ごとの意思決定と運用フロー。互換性・容量・画質のバランスを取り、最小の労力で安定化。