画像Lazy Loadの閾値設計:rootMargin/優先度/INPを両立する最短ガイド
“lazy を付ければ速くなる” は半分正しく半分誤りです。閾値(rootMargin)や優先度を誤ると LCP 候補が遅延し、逆に体感が悪化します。本稿は ①判定 ②閾値 ③優先度 ④検証 を 5 分で確定する最小フレームを提示し、調整の属人性を排除します。
先に結論
ヒーロー/LCP候補=非lazy + priority + fetchpriority=high。折りたたみ直下= rootMargin 400px。長い記事本文サムネ= 600px 以上まとめ読み。
要点(TL;DR)
- 最初に LCP候補セット を明示 (hero, main visual, first article image)。
- 残りを fold直下 / 連続サムネ / 下層 に分類。
- rootMargin は 200 / 400 / 600 (状況で増減)。
- INP懸念: IntersectionObserver コールバックを軽量化(stateバッチ)。
1. 背景:なぜ “lazy全部” が失敗するか
近年ブラウザデフォルト lazy(Chrome)はビューポート近傍のみ先行読み込みします。Hero にまで lazy を付与すると HTTP リクエストが遅延し LCP が伸長。逆に rootMargin を極端に大きくすると eager と変わらない帯域先行で INP が下がるケースもあります。
2. 分類フロー
- Hero/LCP候補を列挙し非lazy化
- 初期 viewport + 1スクロール以内 = fold直下
- 連続サムネ (記事一覧/ギャラリ) をブロック単位で遅延
- 残りは lazy + 大きめ rootMargin (本文末尾)
- 検証: Web Vitals ログ / DevTools waterfall
3. rootMargin プリセット
対象 | rootMargin | 理由 |
---|---|---|
fold直下(1画面先) | 400px | 高速スクロールでも白抜け防止 |
本文中段サムネ | 600px | 連続表示の先行フェッチ |
本文末尾/低優先 | 800px | 視認遅め。先行し過ぎても影響軽微 |
4. 実装スニペット
// observer.ts export function createVisibilityObserver(opts: { rootMargin: string, cb: (el: HTMLElement)=>void }) { const io = new IntersectionObserver((entries) => { for (const e of entries) if (e.isIntersecting) { cb(e.target as HTMLElement); io.unobserve(e.target); } }, { rootMargin: opts.rootMargin }); return { observe: (el: HTMLElement) => io.observe(el) }; }
コールバックでは requestIdleCallback
で decode を遅らせ主スレッドブロックを回避。
5. 優先度ヒント戦略
- LCP候補:
fetchpriority="high"
+ next/image priority - fold直下: 通常 (未指定)
- 下層: lazyのみ (priority付与しない)
- 競合抑制: 高優先度は 1~2 枚に限定
6. INPを悪化させない工夫
大量画像で IO コールバックが集中すると main thread が詰まり入力遅延が跳ねます。バッチ化(setTimeout 0 / idle)と decode 非同期化 (HTMLImageElement.decode) を活用。
7. 公開前チェック(7項目)
- LCP画像に lazy/decoding="async" を付けていない
- Hero 以外の above-the-fold が priority 過多でない
- rootMargin 設定が viewport 高さを極端に超えていない
- スクロール連打で白抜けが起きない
- INP計測 (field) が悪化していない
- Waterfall で LCP画像取得が最前列
- 比較: 一律 lazy 版より LCP 改善 or 同等
8. まとめ
“分類 → rootMargin プリセット → 優先度ヒント最小化 → INP保護” の順に固定すると迷いが消えます。過剰なカスタムロードよりまず基本4段階を再現性高く運用してください。