TECH · srcsetとレスポンシブ画像

端末に必要な1枚だけを配る

サンプルとコード

01

srcset と sizes で1枚だけ配る

単独で開く ↗

候補を srcset に並べ、実際に何px幅で描かれるかを sizes で伝えます。どれが選ばれたかを currentSrc で画面に表示しているので、指定の正しさをその場で確認できます。

HTML
<div class="cards">
  <figure class="card" style="margin:0">
    <img
      alt="幅ごとに異なる候補が読み込まれるサンプル画像"
      width="1600" height="1000"
      sizes="(max-width: 700px) 92vw, 220px"
      srcset="
        w400.svg   400w,
        w800.svg   800w,
        w1600.svg 1600w"
      src="w800.svg" />
    <figcaption>カード内(実描画 約220px)</figcaption>
  </figure>

  <figure class="card" style="margin:0">
    <img
      alt="2枚目のサンプル画像"
      width="1600" height="1000"
      sizes="(max-width: 700px) 92vw, 220px"
      srcset="
        w400.svg   400w,
        w800.svg   800w,
        w1600.svg 1600w"
      src="w800.svg" />
    <figcaption>同じ指定でもう1枚</figcaption>
  </figure>
</div>
CSS
/* 画像の枠は CSS で決め、実際に何px幅で描かれるかを sizes に書く。
   この2つが食い違うと、ブラウザは間違った候補を選ぶ。 */
.cards {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
}
.card {
  background: #fff; border: 1px solid var(--line);
  border-radius: 12px; overflow: hidden;
}
.card img {
  display: block;
  width: 100%;
  height: auto;      /* width/height 属性の比率を保ったまま伸縮させる */
  aspect-ratio: 16 / 10;
  object-fit: cover;
  background: #eef1f6;  /* 読み込み前の面。ここが白飛びを防ぐ */
}
.card figcaption {
  padding: .7rem .85rem;
  font-size: .84rem; line-height: 1.8; color: var(--muted);
}
JavaScript
// currentSrc には「ブラウザが実際に選んだ1枚」が入る。
// srcset の検証はこれを見るのが確実で、開発者ツールより早い。
const out = document.getElementById("out");
function report() {
  const img = document.querySelector(".card img");
  const css = Math.round(img.getBoundingClientRect().width);
  out.textContent =
    "ビューポート幅: " + window.innerWidth + "px / DPR: " + window.devicePixelRatio + "\n" +
    "画像の実描画幅: " + css + "px\n" +
    "必要な解像度: " + Math.round(css * window.devicePixelRatio) + "px\n" +
    "選ばれた候補: " + (img.currentSrc.split("/").pop() || "(未確定)");
}
addEventListener("load", report);
addEventListener("resize", report);
AIへの指示文
srcset と sizes を使って、幅に応じた画像の出し分けを実装してください。
要件:
- 候補は 400w / 800w / 1600w の3つ。srcset に幅記述子付きで並べる。
- sizes には CSS で実際に描かれる幅を書く。
  例: sizes="(max-width: 700px) 92vw, 220px"
  sizes を省くと常に画面幅として計算される、と理由をコメントで書く。
- img には width と height の属性を実寸で書き、CSS 側は
  width: 100% と height: auto にする。
- 読み込み前の面に background を敷いて白飛びを防ぐ。
- JavaScript で img.currentSrc を読み、実際に選ばれた候補・
  ビューポート幅・DPR・必要解像度を画面に表示する。
- 日本語の本文なので line-height は 1.85、letter-spacing は 0 にする。
02

picture で構図ごと変える

単独で開く ↗

解像度ではなく切り取り方そのものを変えます。狭い画面では縦長、広い画面では横長。srcset との使い分けがはっきり分かる形にしました。

HTML
<picture class="hero">
  <!-- 狭い画面には縦長の構図。被写体が小さくならない -->
  <source media="(max-width: 700px)" srcset="tall.svg" width="600" height="700" />
  <!-- 既定は横長。img は必ず最後に置く -->
  <img src="wide.svg" alt="季節のキャンペーン告知" width="1200" height="500" />
</picture>
CSS
/* picture は「同じ被写体を別の構図で見せたい」ときに使う。
   単に解像度を変えたいだけなら img の srcset で足りる。 */
.hero { border-radius: 14px; overflow: hidden; border: 1px solid var(--line); }
.hero img {
  display: block;
  width: 100%;
  height: auto;
  background: #eef1f6;
}
JavaScript
const out = document.getElementById("out");
function report() {
  const img = document.querySelector(".hero img");
  out.textContent =
    "ビューポート幅: " + window.innerWidth + "px\n" +
    "選ばれた構図: " + (img.currentSrc.split("/").pop() || "(未確定)") + "\n" +
    "描画サイズ: " + Math.round(img.getBoundingClientRect().width) + " × " +
    Math.round(img.getBoundingClientRect().height) + "px";
}
addEventListener("load", report);
addEventListener("resize", report);
AIへの指示文
picture 要素で画面幅ごとに構図が変わる画像を実装してください。
要件:
- 狭い画面(max-width: 700px)には縦長の構図、既定は横長の構図。
- source を先に、img を最後に置く。img は必ず必要で、代替と既定を兼ねる。
- source と img の両方に width と height を書く。
- 単に解像度を変えたいだけなら srcset で足りる、という使い分けをコメントで書く。
- currentSrc を読んで、どちらの構図が選ばれたかを画面に表示する。
03

レイアウトを揺らさない

単独で開く ↗

良い例2つと悪い例1つを並べ、実際のCLS値をページ内に表示します。属性を書く方法と aspect-ratio で枠を固定する方法の使い分けが分かります。

HTML
<div class="demo ok">
  <h2><span class="good">良い例</span> width と height を書く</h2>
  <p>属性に実寸を書いておくと、読み込み前から高さが確定します。CSS 側は width:100% と height:auto。</p>
  <img src="w800.svg" alt="" width="800" height="500" />
</div>

<div class="demo ratio">
  <h2><span class="good">良い例</span> aspect-ratio で枠を固定する</h2>
  <p>比率がまちまちの画像を同じ枠に収めたいときは、aspect-ratio と object-fit を組み合わせます。</p>
  <img src="tall.svg" alt="" />
</div>

<div class="demo bad-demo">
  <h2><span class="bad">悪い例</span> 何も指定しない</h2>
  <p>読み込みが終わった瞬間に高さが跳ね、この下の文章が押し下げられます。</p>
  <img src="wide.svg" alt="" />
  <p class="after">この行が、画像の読み込み完了で下へ動きます。</p>
</div>
CSS
/* width と height の属性を書いておくと、ブラウザは読み込み前に
   縦横比を計算して場所を確保する。CSS で幅だけ 100% にしても、
   height: auto があれば比率は保たれる。 */
.ok img {
  display: block;
  width: 100%;
  height: auto;          /* 属性の比率を維持したまま伸縮する */
  border-radius: 10px;
  background: #eef1f6;   /* 読み込み中の面。白飛びを防ぐ */
}

/* 属性を書けない場合(枠の比率を固定したい場合)は aspect-ratio。
   object-fit と併せると、比率の違う画像でも枠が動かない。 */
.ratio img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  border-radius: 10px;
  background: #eef1f6;
}

/* 悪い例。属性も aspect-ratio も無いと、読み込み完了の瞬間に
   高さが 0 から実寸へ跳ね、下の文章が押し下げられる。 */
.bad-demo img {
  display: block;
  width: 100%;
  border-radius: 10px;
  background: #eef1f6;
}
JavaScript
// CLS は PerformanceObserver で自分でも測れる。
// hadRecentInput が true のずれ(利用者の操作が原因)は除外する。
let cls = 0;
const box = document.getElementById("cls");
new PerformanceObserver((list) => {
  for (const e of list.getEntries()) if (!e.hadRecentInput) cls += e.value;
  box.textContent =
    "このページの CLS: " + cls.toFixed(4) +
    (cls > 0.1 ? "(0.1 超。不合格)" : "(0.1 以下。良好)");
}).observe({ type: "layout-shift", buffered: true });
AIへの指示文
画像によるレイアウトシフトを防ぐ書き方を、良い例と悪い例で比較してください。
要件:
- 良い例1: width と height の属性を実寸で書き、CSS は width:100% height:auto。
- 良い例2: aspect-ratio と object-fit で枠の比率を固定する。
  比率がまちまちの画像を同じ枠に収めたいときの方法として示す。
- 悪い例: 属性も aspect-ratio も無い状態。読み込み完了で下の文章が
  押し下げられることを、実際に文章を置いて示す。
- PerformanceObserver で layout-shift を購読し、CLS の実測値を
  ページ内に表示する。hadRecentInput が true のものは除外する。
- 0.1 を超えると Core Web Vitals で不合格になる旨を表示に含める。
04

読み込み優先度の使い分け

単独で開く ↗

loading・fetchpriority・decoding は役割が違います。取り違えると主役の画像がいちばん遅くなります。LCP がどの要素になったかも実測表示します。

HTML
<table class="tbl">
  <thead><tr><th>属性</th><th>役割</th><th>使いどころ</th></tr></thead>
  <tbody>
    <tr>
      <td><code>loading</code></td>
      <td>いつ取りに行くか</td>
      <td>画面外なら <code>lazy</code>。<strong>最初の画面に入る画像には付けない</strong></td>
    </tr>
    <tr>
      <td><code>fetchpriority</code></td>
      <td>他の要求より先に取るか</td>
      <td>LCP になる1枚だけ <code>high</code>。全部に付けたら意味が消える</td>
    </tr>
    <tr>
      <td><code>decoding</code></td>
      <td>復号で描画を止めるか</td>
      <td>基本 <code>async</code>。描画をブロックしない</td>
    </tr>
  </tbody>
</table>

<div class="strip">
  <!-- 最初の画面に入る主役。lazy を付けず、優先度を上げる -->
  <img src="wide.svg" alt="ページ先頭の主役画像"
       width="1200" height="500"
       fetchpriority="high" decoding="async" />

  <!-- すぐ下に続くが画面内。lazy にはしない -->
  <img src="w800.svg" alt="続く画像"
       width="800" height="500" decoding="async" />
</div>

<div class="spacer"></div>

<!-- ここから下は初期表示に入らない。lazy が効く -->
<div class="strip">
  <img src="w400.svg" alt="スクロールして初めて見える画像"
       width="400" height="250" loading="lazy" decoding="async" />
  <img src="tall.svg" alt="さらに下の画像"
       width="600" height="700" loading="lazy" decoding="async" />
</div>
CSS
.strip { display: grid; gap: .8rem; margin: 1.2rem 0 0; }
.strip img {
  display: block; width: 100%; height: auto;
  border-radius: 10px; background: #eef1f6;
}
.spacer { height: 120vh; }
JavaScript
// LCP がどの要素になったかは自分で測れる。
// 「主役だと思っていた画像」と実際の LCP がずれていることは多い。
const out = document.getElementById("out");
let lcp = null;
new PerformanceObserver((l) => {
  for (const e of l.getEntries()) lcp = e;
  render();
}).observe({ type: "largest-contentful-paint", buffered: true });

function render() {
  const imgs = [...document.images];
  const lazy = imgs.filter((i) => i.loading === "lazy").length;
  out.textContent =
    "画像 " + imgs.length + "枚 / うち lazy " + lazy + "枚\n" +
    "LCP: " + (lcp ? Math.round(lcp.startTime) + "ms" : "計測中") + "\n" +
    "LCP の要素: " + (lcp?.element?.tagName ?? "-") +
      (lcp?.url ? " (" + lcp.url.split("/").pop() + ")" : "");
}
addEventListener("load", render);
AIへの指示文
画像の読み込み優先度を正しく指定したページを作ってください。
要件:
- 3つの属性の役割を表にする。
  loading はいつ取りに行くか、fetchpriority は他より先に取るか、
  decoding は復号で描画を止めるか。
- 最初の画面に入る主役の画像には loading="lazy" を付けず、
  fetchpriority="high" を1枚だけに付ける。全部に付けたら意味が消える。
- 画面外の画像には loading="lazy" を付ける。
- すべてに decoding="async" を付ける。
- PerformanceObserver で largest-contentful-paint を購読し、
  LCP の時刻と、実際に LCP になった要素を画面に表示する。

この技術について

レスポンシブ画像の目的は、見た目を整えることではありません。その端末に必要な1枚だけを配ることです。

このサイト自身が分かりやすい実例でした。カードの実描画幅は 372px なのに、配っていたのは 1200px の画像。面積にして約10倍のデータを、誰も必要としていないのに送り続けていました。srcset を入れただけで、1x のデスクトップでは 1枚あたり 84KB から 13KB へ、一覧全体では 4.7MB から 0.99MB へ落ちています。

ブラウザに渡す情報は2つだけ

仕組みは単純です。候補を並べるのが srcset実際に何px幅で描かれるか教えるのが sizes。ブラウザはこの2つと端末の解像度から、最適な1枚を自分で選びます。

つまずくのは決まって sizes です。省略すると 100vw、つまり画面幅いっぱいに描かれる前提で計算されます。220px のカードに置いた画像でも、1600px の候補が選ばれる。srcset を書いたのに転送量が減らないという相談は、ほぼこれが原因です。

正しく効いているかは img.currentSrc を読めば分かります。開発者ツールを開くより速く、条件を変えながら即座に確認できます。この記事のサンプルはすべて画面上に表示しているので、ウィンドウ幅を変えて試してみてください。

srcset は用意した画像と実寸、sizes は表示される幅の見込み。選ぶのはブラウザ srcset 用意した画像と、その実寸 photo-400.jpg 400w photo-800.jpg 800w photo-1200.jpg 1200w sizes 表示される幅の見込み (min-width: 900px) 33vw (min-width: 600px) 50vw 100vw どれを落とすかはブラウザが決める 端末の解像度・回線・キャッシュまで見て選ぶ。こちらは候補と見込み幅を渡すだけ
渡すのは事実だけで、選択には関与しません。sizes を書き忘れると全幅(100vw)とみなされ、小さく表示する画像にも大きいファイルが落ちてきます。

解像度の話と、構図の話は別

srcset が扱うのは「同じ絵の解像度違い」です。切り取り方そのものを変えたいときは picture を使います。

横長のヒーロー画像をスマートフォンでそのまま縮めると、被写体が小さくなりすぎて何の写真か分からなくなる。こういう場面では、狭い画面用に縦長へ切り直した別の絵を用意します。source に条件を書き、img は最後に置く。img は代替と既定を兼ねるので、省略できません。

srcset は同じ絵の大きさ違い、picture は絵そのものを差し替える 解像度の話 — srcset 同じ絵。大きさだけが違う 構図の話 — picture 絵そのものが違う。寄って切り直す 狭い画面で人物が豆粒になるなら、縮小ではなく別の絵に差し替える場面 <picture><source media=…><img src=… /></picture>
見分け方は一つです。絵の中身が同じなら srcset、切り取り方まで変えたいなら picture。迷ったら、狭い画面で主役が判別できるかを見てください。

場所を先に確保する

画像で最も損をしやすいのは、レイアウトシフトです。読み込みが終わった瞬間に高さが 0 から実寸へ跳ね、読んでいた文章が下へ流れる。これは Core Web Vitals の CLS として計測され、0.1 を超えると不合格になります。

防ぎ方は2通りあります。widthheight属性を実寸で書くか、aspect-ratio で枠の比率を固定するか。前者は「元の比率を保ちたい」とき、後者は「比率がまちまちの画像を同じ枠に収めたい」とき。CSS で width: 100% にしても、height: auto があれば属性の比率は保たれます。

読み込み中の面に背景色を敷いておくのも地味に効きます。白のまま放置すると、暗い画像が現れる瞬間に画面が明滅します。

width と height を書くと、画像が届く前に場所が確保されてレイアウトがずれない 寸法を書かない 画像が届いた瞬間に 下の文章が突き落とされる width と height を書く 場所を先に確保 width と height は「表示サイズ」ではなく比率の申告。 CSS 側は width:100%; height:auto のままでよい
読んでいる最中に文章が動くと、行を見失います。属性を2つ書くだけで、この現象は起きなくなります。表示の大きさは今までどおり CSS 側で決めて構いません。

3つの属性を取り違えない

最後に、優先度の指定です。役割が違うので、混ぜて覚えると事故ります。

属性何を決めるか判断
loadingいつ取りに行くか画面外なら lazy最初の画面に入る画像には付けない
fetchpriority他の要求より先に取るかLCP になる1枚だけ high
decoding復号で描画を止めるか基本 async

いちばん多い失敗は、一覧ページの全画像に loading="lazy" を付けることです。最初の画面に入っている画像まで「必要になるまで取りに行かない」扱いになり、LCP が悪化します。最初の数枚だけ外す、という運用にしてください。fetchpriority="high" も同様で、全部に付ければ何も優先されなくなります。1ページに1枚だけです。

loading と fetchpriority と decoding は効く層が違う loading="lazy" 画面に近づくまで読み込まない 最初の1枚には付けない 折り返しの下にある画像に付ける。上にある画像に付けると、表示が遅れて逆効果になる fetchpriority="high" 先に取ってほしい1枚を指名 付けるのは1枚だけ 全部に付けると優先順位が無くなり、指定していないのと同じ結果になる decoding="async" 展開で他の描画を止めない 迷ったら付けてよい 読み込みの順番ではなく、届いたあとの展開の話。上の2つとは別の層
3つとも名前が似ていますが、効く場面がそれぞれ違います。「速くしたいから全部付ける」が最も多い失敗で、とくに上2つは付ける対象を絞ることに意味があります。

使いどころとつまずきどころ

向いている場面

  • 写真やサムネイルを多数並べる一覧ページ
  • 回線の細い環境も想定する一般向けサイト
  • 画面幅で構図を変えたいヒーロー画像
  • Core Web Vitals の LCP を詰めたいとき

つまずきやすい点

  • sizes を書かないと常に画面幅として計算される
  • width と height を省くとレイアウトシフトが起きる
  • LCP になる画像に loading=lazy を付けると遅くなる
  • art direction が要るのに srcset だけで済ませようとする

よくある質問

sizes を省略するとどうなりますか。

常に 100vw、つまり画面幅いっぱいに描かれる前提で候補が選ばれます。実際には220pxのカードに置いている画像でも、1600pxの候補が読み込まれることになります。srcset を書いたのに転送量が減らない場合、たいてい原因はここです。

srcset と picture はどちらを使えばいいですか。

同じ絵の解像度違いを配りたいだけなら srcset、切り取り方や構図そのものを変えたいなら picture です。横長のヒーロー画像をスマートフォンでそのまま縮めると被写体が小さくなりすぎる、という場合が picture の出番になります。

LCP になる画像に loading=lazy を付けてはいけませんか。

付けてはいけません。lazy は「必要になるまで取りに行かない」指示なので、最初の画面に入る画像に付けると読み込みの開始そのものが遅れ、LCP が悪化します。一覧ページでは最初の数枚だけ lazy を外す、という運用が現実的です。

実際にどの候補が選ばれたか確認する方法はありますか。

img.currentSrc を読むのが最も確実です。開発者ツールのネットワークタブでも分かりますが、currentSrc なら条件を変えながら即座に確認できます。このページのサンプルはすべて画面上に表示しているので、ウィンドウ幅を変えて試せます。

同じ技術を使った完成例を、画面の型ごとにまとめています。作りたい画面が決まっているなら、こちらから探すほうが早いはずです。

ほかの技術