画像読み込み戦略の設計 - preload, fetchpriority, decoding を使いこなす
画像読み込みの優先度制御が必要な理由 - ブラウザの限界を理解する
ブラウザは HTML を解析する際、プリロードスキャナーと呼ばれる仕組みで <img> タグを先読みし、画像のダウンロードを開始します。しかし、この自動的な優先度付けには限界があります。ブラウザはページのレイアウトが確定するまで、どの画像がビューポート内に表示されるか (つまり LCP 候補か) を正確に判断できません。
Chrome のリソース優先度は 5 段階 (Highest, High, Medium, Low, Lowest) で管理されています。デフォルトでは、ビューポート内の画像は High、ビューポート外の画像は Low に分類されます。しかし、レイアウト計算が完了するまでこの判定は行われないため、初期段階では全画像が Medium として扱われ、最適な優先度付けが遅延します。
この問題は特に以下のケースで顕著になります。CSS 背景画像は HTML のプリロードスキャナーが検出できないため、CSS の解析完了まで発見されません。JavaScript で動的に挿入される画像も同様です。また、<picture> 要素内の画像は、メディアクエリの評価後に初めてダウンロード対象が確定するため、単純な <img> より発見が遅れます。
これらの課題に対して、開発者が明示的に優先度を指示する手段が preload、fetchpriority、decoding の 3 つの属性です。それぞれ異なるレイヤーで画像の読み込みを最適化し、組み合わせることで LCP を 500ms-1 秒以上改善できるケースがあります。
preload - 画像の早期発見を実現するリソースヒント
<link rel="preload"> は、ブラウザに「このリソースはすぐに必要になる」と事前に通知するリソースヒントです。HTML の <head> セクションに記述することで、DOM 解析の最初期段階でリソースの取得を開始させます。
画像の preload が特に効果的なケースは 3 つあります。第一に、CSS の background-image で指定された画像です。通常、CSS ファイルのダウンロード → 解析 → CSSOM 構築 → レンダーツリー構築という一連のプロセスが完了するまで画像の存在が認識されません。preload を使えば、この待ち時間を完全にスキップできます。
第二に、JavaScript で動的に生成される画像です。SPA (Single Page Application) のヒーロー画像や、カルーセルの最初の画像がこれに該当します。第三に、<picture> 要素で条件分岐する画像です。
実装例: <link rel="preload" as="image" href="/images/hero.avif" type="image/avif" fetchpriority="high">
レスポンシブ画像の preload には imagesrcset と imagesizes 属性を使用します: <link rel="preload" as="image" imagesrcset="hero-400.avif 400w, hero-800.avif 800w, hero-1200.avif 1200w" imagesizes="100vw" type="image/avif">
注意点: preload は強力ですが、乱用すると逆効果です。preload されたリソースは最優先でダウンロードされるため、CSS や JavaScript など他の重要リソースの帯域を奪います。原則として、LCP 画像 1 枚のみに限定してください。Chrome DevTools の Console に「preload されたリソースが 3 秒以内に使用されなかった」という警告が出る場合は、不要な preload を削除すべきです。
fetchpriority - リソース優先度の明示的な制御
fetchpriority 属性は、同じ種類のリソース間での相対的な優先度をブラウザに伝えます。値は high、low、auto (デフォルト) の 3 つです。preload が「いつ発見するか」を制御するのに対し、fetchpriority は「発見後にどの順番で取得するか」を制御します。
fetchpriority="high" の効果は、Chrome の内部優先度を Medium から High に引き上げることです。これにより、画像のダウンロードが CSS や同期スクリプトと同等の優先度で実行されます。LCP 画像に適用した場合、実測で 100-400ms の LCP 改善が報告されています。
具体的な使い分け:
fetchpriority="high": LCP 候補のヒーロー画像、ファーストビューの主要画像に使用。ページあたり 1-2 枚に限定します。fetchpriority="low": フッターの画像、カルーセルの 2 枚目以降、装飾的な背景画像に使用。これらの優先度を下げることで、重要な画像の帯域を確保します。fetchpriority="auto": デフォルト値。ブラウザの自動判定に委ねます。大半の画像はこの設定で問題ありません。
実装例: <img src="hero.jpg" fetchpriority="high" alt="メインビジュアル" width="1200" height="600">
fetchpriority は <img>、<link rel="preload">、<script>、<iframe> に適用可能です。画像以外のリソースにも使えるため、ページ全体のリソース優先度を統合的に設計できます。例えば、ファーストビューに不要な第三者スクリプトに fetchpriority="low" を設定し、LCP 画像の帯域を確保する戦略が有効です。
decoding 属性 - 画像デコードとメインスレッドの関係
decoding 属性は、画像のデコード処理 (圧縮データからピクセルデータへの変換) をメインスレッドで同期的に行うか、非同期的に行うかを制御します。値は sync、async、auto (デフォルト) の 3 つです。
decoding="async" を指定すると、画像のデコードがメインスレッドをブロックしなくなります。これにより、画像のデコード中も DOM の構築やスクリプトの実行が継続され、ページ全体のレンダリングが高速化します。特に大きな画像 (2,000 px 以上) や、複数の画像が同時にデコードされる場面で効果が顕著です。
decoding="sync" は、画像のデコードが完了するまで後続のレンダリングをブロックします。これは、画像が表示される瞬間にデコード済みであることを保証したい場合に使用します。例えば、ページ遷移時のヒーロー画像で「画像が一瞬ぼやけて表示される」現象を防ぎたい場合です。ただし、メインスレッドのブロックは INP (Interaction to Next Paint) に悪影響を与えるため、慎重に使用してください。
実用的な指針として、ほとんどの画像には decoding="async" を推奨します。ブラウザのデフォルト (auto) は多くの場合 async と同等の動作をしますが、明示的に指定することで意図を明確にし、ブラウザ間の挙動差を排除できます。
注意すべき点として、decoding="async" は画像の「ダウンロード」には影響しません。あくまで「ダウンロード完了後のデコード処理」の実行方法を制御するだけです。ダウンロードの優先度を制御したい場合は fetchpriority を、ダウンロードの開始タイミングを制御したい場合は preload や loading を使用します。
3 つの属性の組み合わせ - LCP 最適化の実践パターン
preload、fetchpriority、decoding は互いに補完する関係にあり、適切に組み合わせることで最大の効果を発揮します。代表的なパターンを紹介します。
パターン 1: CSS 背景画像の LCP 最適化
<head> に <link rel="preload" as="image" href="hero-bg.avif" type="image/avif" fetchpriority="high"> を記述し、対応する要素に background-image を設定します。preload で早期発見 + fetchpriority で最優先取得を実現します。
パターン 2: ファーストビューの img 要素
<img src="hero.jpg" fetchpriority="high" decoding="async" loading="eager" width="1200" height="600" alt="..."> - fetchpriority で優先度を上げ、decoding="async" でメインスレッドのブロックを防ぎ、loading="eager" (デフォルト) で遅延読み込みを無効化します。
パターン 3: ビューポート外の画像
<img src="below-fold.jpg" loading="lazy" fetchpriority="low" decoding="async" width="800" height="400" alt="..."> - loading="lazy" でビューポート進入まで読み込みを遅延し、fetchpriority="low" で他の画像との帯域競合を回避します。
パターン 4: カルーセルの最初の画像
最初のスライドのみ fetchpriority="high" + loading="eager"、2 枚目以降は fetchpriority="low" + loading="lazy" を設定します。ユーザーが最初に目にする画像だけを優先的に読み込み、残りはインタラクション後に取得します。
これらのパターンを組み合わせた実測結果として、EC サイトの商品一覧ページで LCP が 3.2 秒から 1.8 秒に改善 (44% 短縮) した事例があります。
実装時の注意点とデバッグ方法
画像読み込み戦略の実装で陥りがちなミスと、その検証方法を解説します。
よくあるミス 1: LCP 画像に loading="lazy" を設定 - これは最も頻繁に見られるパフォーマンス劣化の原因です。loading="lazy" は画像がビューポートに近づくまでダウンロードを遅延させるため、LCP 画像に適用すると 300-500ms の遅延が発生します。Chrome DevTools の Performance パネルで LCP 要素を特定し、その画像に lazy が付いていないか確認してください。
よくあるミス 2: preload の過剰使用 - 3 枚以上の画像を preload すると、帯域の奪い合いが発生し、かえって全体が遅くなります。preload は LCP 画像 1 枚に限定するのが原則です。Console に「The resource was preloaded using link preload but not used within a few seconds」の警告が出ていないか確認します。
よくあるミス 3: fetchpriority="high" の乱用 - 全画像に high を設定すると、優先度の差別化が無意味になります。ページあたり 1-2 枚に限定してください。
デバッグ方法: Chrome DevTools の Network パネルで Priority 列を表示し、各画像の実際の優先度を確認します。Waterfall チャートで画像のダウンロード開始タイミングと完了タイミングを視覚的に確認し、LCP 画像が最初にダウンロード完了しているか検証します。Lighthouse の「Largest Contentful Paint element」セクションで LCP 要素と改善提案を確認し、WebPageTest の filmstrip view で実際のレンダリング過程を 100ms 単位で追跡します。
計測ツール: web-vitals ライブラリを使用して RUM データを収集し、fetchpriority 導入前後の LCP 分布を比較します。p75 値で 200ms 以上の改善が確認できれば、施策は成功と判断できます。