srcset と sizes の仕組みを完全理解して、最適な画像を配信する

srcset と sizes の仕組みを完全理解して、最適な画像を配信する

想定読者

  • 画像の表示を速くしたい人

  • srcset で意図した解像度が選ばれず困っている人

  • メディアクエリで画像サイズを切り替えたい人

この記事で得られること

  • srcset がどのようにして画像候補を選択するのかを体系的に理解できます。

  • sizes の書き方と、ビューポート・DPR に応じた最適値の決め方がわかります。


導入

サイトのパフォーマンス指標 LCP (Largest Contentful Paint) を改善しようとした際、私は srcset に用意した複数解像度の画像から “狙った 1 枚” を選ばせることができずに悩みました。調べてみると、ブラウザが行う画像選択ロジックを正しく理解していなかったことが原因でした。

この記事では、私が行ったデバッグ方法とともに srcset / sizes の選択アルゴリズムを噛み砕いて解説します。


React コンポーネント例

以下は実際にブログヒーロー画像を描画しているコンポーネント例です。<picture> 内で AVIF → WebP → img fallback の順に指定しています。

import { useEffect, useRef } from "react";
import style from "./BlogHero.module.css";

export const BlogHero = () => {
  const imgRef = useRef<HTMLImageElement>(null);

  // デバッグ用:ブラウザが選択した値をコンソールに出力
  useEffect(() => {
    if (!imgRef.current) return;
    const { currentSrc, sizes } = imgRef.current;
    console.table({
      currentSrc,
      innerWidth: window.innerWidth,
      sizes,
      DPR: window.devicePixelRatio,
      slotWidth: Math.round(imgRef.current.getBoundingClientRect().width) + " px (actual)"
    });
  }, []);

  const sizes = "(min-width:768px) 45vw, 36vw"; // >=768px では 45vw、それ以外は 36vw

  return (
    <picture>
      <source
        type="image/avif"
        srcSet="/img/hero/blog/kameka_blog@400.avif 400w,
                /img/hero/blog/kameka_blog@800.avif 800w,
                /img/hero/blog/kameka_blog@1200.avif 1200w"
        sizes={sizes}
      />
      <source
        type="image/webp"
        srcSet="/img/hero/blog/kameka_blog@400.webp 400w,
                /img/hero/blog/kameka_blog@800.webp 800w,
                /img/hero/blog/kameka_blog@1200.webp 1200w"
        sizes={sizes}
      />
      <img
        ref={imgRef}
        width={1200}
        height={800}
        src="/img/hero/blog/kameka_blog@400.webp" // 最低限のフォールバック
        alt="ブログヒーロー画像"
        srcSet="/img/hero/blog/kameka_blog@400.webp 400w,
                /img/hero/blog/kameka_blog@800.webp 800w,
                /img/hero/blog/kameka_blog@1200.webp 1200w"
        sizes={sizes}
        className={style.heroImg}
      />
    </picture>
  );
};

srcset の書式と「幅記述子」

/img/...@400.avif  400w
/img/...@800.avif  800w
  • URL の後ろに続く 400w や 800w は 幅記述子 (width descriptor) と呼ばれます。

  • ブラウザが「この候補は横幅 400 CSS ピクセル以上必要なときに使える」と理解する ための手がかりです。

  • ピクセル密度記述子 (2x など) も指定できますが、本記事では幅記述子に統一します。

sizes の書式と役割

(min-width:768px) 45vw, 36vw
  1. メディア条件 (min-width:768px) を評価。

  2. 条件が true のとき ソースサイズ値 45vw を、false のとき 36vw を「想定レンダリング幅」として採用します。

  3. 実際にレンダリングされる幅 ではなく、あくまでも srcset 候補を選ぶ入力値 です。

console.table の出力項目解説

デバック用に以下の項目を出力するようにしています。

項目 説明
currentSrc ブラウザが最終的に読み込んだ画像 URL です。どの候補が選ばれたのかを即座に確認できます。
innerWidth window.innerWidth で得られるビューポート幅 (CSS ピクセル)。sizes のメディア条件判定に使われます。
sizes sizes 属性そのもの(評価前の文字列)。どのように意図をブラウザへ伝えているか再確認できます。
DPR window.devicePixelRatio。端末の物理ピクセル密度を示し、必要ピクセル計算に乗算されます。
slotWidth getBoundingClientRect().width で取得した、実際にレンダリングされた要素の幅。理論値 (sizes × DPR) と比較してズレを検証できます。

実際の出力結果

下記の値が実際に出力されました。アルゴリズムの説明を確認する際にご参考にしてください。

{
    "currentSrc": "http://localhost:3000/img/hero/blog/kameka_blog@400.avif",
    "innerWidth": 412,
    "sizes": "(min-width:768px) 45vw, 36vw",
    "DPR": 2.625,
    "slotWidth": "346 px (actual)"
}

ブラウザが行う画像選択アルゴリズム

  1. sizes の評価

    • 例:window.innerWidth = 412px(min-width:768px)false36vw

    • 計算後:412 * 0.36 ≒ 148.3px

  2. DPR (Device Pixel Ratio) を乗算

    • 例:devicePixelRatio = 2.625

    • 必要ピクセル数:148.3 * 2.625 ≒ 389.3px

  3. srcset 候補から最小の "≥ 必要ピクセル" を探す

    • 400w が最初に条件を満たす → /kameka_blog@400.avif が選択。

まとめ

  • sizes は「どのくらいの幅でレンダリングする予定か」をブラウザに伝えるヒント。

  • ブラウザは sizes × DPR を計算し、srcset の幅記述子リストを小さい方から走査して “最初に条件を満たす 1 枚” を選択します。

  • 迷ったときは DevTools と console.tablecurrentSrc / sizes / DPR / slotWidth を確認すると原因が一目瞭然。

これで srcset / sizes の仕組みが腑に落ち、狙いどおりの画像を配信できるはずです。LCP などのパフォーマンス改善に役立ててみてください!