💯

過不足のない画像サイズで表示したい時に知っておきたいsrcsetとsizesの基本とパターン

2024/11/18に公開
1

表示速度の高速化を考える時に最優先になるのが画像の最適化。ある記事によると、ページ全体の約44%を画像が占めているそうなので、その重要性がわかります。
画像の最適化には次のような方法があると考えています。

  • 適切な画像形式を使う
  • 画像のファイルサイズを削減する
  • 過不足のない画像サイズで表示する
  • 遅延読み込みで必要なタイミングで読み込ませる

この記事では「過不足のない画像サイズで表示する」方法に関する内容をまとめています。

言葉の定義

この記事では次のように表現します。

  • 表示領域:
    • ブラウザ上で画像が表示される時のサイズ
    • 例:この画像はモバイルでは100px × 100px、デスクトップでは80px × 80pxで表示される
  • 画像サイズ:
    • 画像ファイル自体が持っているピクセルの寸法
    • 例:この画像は1280px × 720pxのサイズを持っている
  • デバイスピクセル比:
    • 物理ピクセルとCSSピクセルの比率で大きいほど高精細
    • 画像サイズ ÷ 表示領域から計算ができる
    • デバイスのデバイスピクセル比はwindow.devicePixelRatioで取得できる
    • 例1:画像サイズが100pxで表示領域が50pxだからデバイスピクセル比は2で表示される
    • 例2:iPhone 16のデバイスピクセル比は3だ

srcsetとsizesの基本

表示領域・画像サイズ・デバイスピクセル比に応じて最適な画像を表示させたい場合、srcsetやsizesを使って指定します。
簡潔に表現すると次のような役割や機能を持った属性です。

  • srcset:画像の候補と、その画像のサイズかデバイスピクセル比を記述する
  • sizes:表示領域のサイズを記述する

srcsetでは2つの記述子で指定します。

  • w記述子:その画像がどれだけの画像サイズを持っているか
  • x記述子:その画像をどのデバイスピクセル比の場合に適用するか

srcsetではwとxのいずれかの記述子で統一する必要がありsizesはw記述子の場合しか適用されません。よって、指定するパターンはどちらかになります。

srcsetのx記述子だけで指定するパターン。

<img
  src="image_2x.webp"
  srcset="image_1x.webp 1x, image_2x.webp 2x"
  alt=""
>

srcsetのw記述子とsizesで指定するパターン。

<img
  src="image_320w.webp"
  srcset="image_320w.webp 320w, image_768w.webp 768w, image_1024w.webp 1024w"
  sizes="(max-width: 767px) calc(100vw - 40px), calc(768px - 40px)"
  alt=""
>

srcsetのx記述子だけで指定するパターン

<img
  src="image_2x.webp"
  srcset="image_1x.webp 1x, image_2x.webp 2x"
  alt=""
>

x記述子を使うと、デバイスピクセル比が1だったらこの画像、2だったらこの画像、といったように指定できます。

x記述子が最適解になるのは、表示領域がメディアクエリにかかわらず同じである場合や、変動が許容できるほど小さい場合です。
たとえば表示領域が常に50pxの場合です。デバイスピクセル比が1だったら50pxの画像、2だったら100pxの画像が必要になります。
次のように指定しておけば常にデバイスピクセル比に一致した画像が表示されます。

<img
  src="image_100w.webp"
  srcset="image_50w.webp 1x, image_100w.webp 2x"
  alt=""
>

x記述子は必要な画像が最小限に抑えられるメリットはありつつ、表示領域の差が大きい場合に最適化できない可能性があります。
表示領域がモバイルで50px、デスクトップで80pxだったとします。

デバイスピクセル比:1 デバイスピクセル比:2
50px 100px
80px 160px

デバイスピクセル比が2の場合、モバイルは100pxで足りますが、デスクトップでは160pxが必要になります。つまり、モバイルで最適化するとデスクトップでデバイスピクセル比が足りなくなり、デスクトップで最適化するとモバイルでデバイスピクセル比が必要以上に大きくなってしまいます。

どちらに合わせるかはプロジェクト次第です。どちらの割合が多いのか、高速化を優先するのか、きれいに表示されることを優先して多少のサイズアップは許容できるのか。そういったことが検討するポイントになると思います。
あるいは、w記述子での最適化を検討したほうがいいかもしれません。

srcsetのw記述子とsizesで指定するパターン

x記述子は指定が単純でしたが、w記述子は少し複雑になります。
どのような指定方法になっているのか次のコードで見ていきます。

<img
  src="image_320w.webp"
  srcset="image_320w.webp 320w, image_768w.webp 768w, image_1024w.webp 1024w"
  sizes="(max-width: 767px) calc(100vw - 40px), 700px"
  alt=""
>

srcset内のimage_320w.webp 320wに注目してください。この画像は横幅320pxで作成されています。そして 320wと後に続けると「この画像は横幅320pxの画像サイズがあります」とブラウザに伝えることができます。
ただし、あくまで画像が本来持っているサイズを伝えているのであって、表示領域を伝えているわけではないことに注意してください。
表示領域に関してはsizesで指定します。

sizesの初期値は100vwです。つまり、srcsetで適切に画像サイズを伝えたとしても、基準になる表示領域は画面いっぱいになってしまいます。
画像を100vwで表示することは多くありません。ほとんどの場合で左右に余白が入っていたり、グリッドになっていたり、コンテンツの最大幅が制限されているはずです。ですので、ほとんどの場合で大きすぎる画像が表示されることになってしまいます。
w記述子で最適化するための最重要ポイントは「sizesで適切な表示領域を指定すること」です。

上記の例ではsizes="(max-width: 767px) calc(100vw - 40px), 728px"と指定しています。
max-width: 767pxでは表示領域を100vwから左右の余白20pxずつを差し引いた値としています。このメディアクエリに該当しない場合はコンテンツ幅が728pxに固定される想定のもと、その値を表示領域に指定しているイメージです。

ただしこの場合も、表示領域をコンテンツ幅に合わせた指定をしているだけで、画像によって表示領域は変わってきます。

いちばんわかりやすいのが画像が固定の場合です。
モバイルで50px、それ以上で100pxの場合はsizes="(max-width: 767px) 50px, 100px"と指定しておけばOKです。デバイスピクセル比も考慮してブラウザ側がsrcsetから適切な画像を選択してくれます。
あるいは、常に50px固定なら50px"のようにメディアクエリすら不要です。

ややこしいのが、フルードイメージかつ自由にカラムを変更できるグリッドレイアウトです。
本来であればsizes内にはcalc()以外にもmin()max()も使用できるのですが、Safariが対応していません。それとメディアクエリは横幅のみ指定が可能なので、resolutionでデバイスピクセル比を判定することもできません
使用できるCSSの単位は<length>である必要があるので、emexchremvwvhvminvmaxなどが使えます(%は使えません)絶対値やビューポートを基準にはできますが、コンテナーサイズの判定ができないという箇所が重要ですね。%が使えないのは、そもそも<length>ではなく<percentage>なこともありますが、コンテナーを基準にした計算が必要になるからだと思います。なので、widthstretchキーワードなどもコンテナーに対する基準なので使えるようにはならないでしょう。
こういった条件を踏まえながら表示領域をうまく指定する必要があります。モバイルはコンテンツ幅=100vwと考えていいので指定しやすいですが、デスクトップなどはコンテンツにmax-widthを指定している場合がほとんどなので、うまく計算するよりも実際の表示領域を測ってpx指定することが多くなると思います。

srcsetに指定する画像はどのように用意するか

ここまではsrcsetやsizesの指定についての話でしたが、運用を考えるとsrcsetに指定する画像サイズのパターンと、それをどのように用意しておくかの検討が必要です。

npm scriptsを使う

フロントエンド側だけで簡易に対応したい場合は、npm scriptsで画像のリサイズと最適化をするのがいいかなと思います。
具体的なコードは載せないですが、デフォルトの出力パターンとディレクトリごとの出力パターンを定義しておいて、WebP変換と圧縮、パターンに応じて複数枚の画像をリサイズで生成するようなイメージです。

Next.jsのImageコンポーネントを使う

Next.jsを使っている場合は<Image>の使用を検討できます。
sizesの指定もできますし、Vercelを使っていればsrcsetを自動で出力してくれます。料金はsrcに指定した画像ごとに、最初の5000枚は無料、以降は1000枚ごとに月額5ドルとなっています。

sercsetに出力される画像は、deviceSizesとimageSizesの配列によって変わります。
たとえばsizesを指定せずにwidth={300}と指定した場合、300以上にいちばん近い384と、300の2倍である600以上にいちばん近い640が選択されます。

deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],

deviceSizesは名前の通り想定されるデバイスの横幅のサイズです。
imageSizesはデバイスの全幅よりも小さいサイズです。16の倍数で用意されています。
まず考えるのは最大幅です。コンテンツの最大幅の2倍以上を用意する必要はほとんどの場合でありません。そしてコンテンツの最大幅も追加して、それに近い値は削除でいいと思います。
小さい画像に関しては、コンポーネント間でサイズをルール化していたらそれを当てはめて、決まりがないのであればデフォルトの値のまま使ってもいいかもしれません。

Vercel以外のCDNを使う場合はloaderの作成が必要なようです。

CloudFront Functions + Lambdaでリアルタイムに画像を生成する

このあたりは専門外ですが、次の記事のように画像変換基盤を作っておくと、フロントエンド側は画像にクエリパラメータを書いておくだけで指定した画像サイズを取得できるようになります。

https://zenn.dev/dely_jp/articles/3de1382c320350

まとめ

  • x記述子は表示領域が1つ、あるいは必要な画像サイズが大きく変わらない場合に有効
  • w記述子は最適化するパターンを増やせる反面、適切なsizes指定が必要
  • sizes内はメディアクエリ、calc()vwpxを駆使する
  • srcsetに指定する画像はバックエンドやインフラ側も考慮しつつ準備が必要
  • srcsetに指定するサイズのパターンはNext.jsのdeviceSizesとimageSizesの配列を参考にしながらプロジェクトにあった値に調整する

srcsetとsizesによる画像の最適化をやってみて感じたのは、完璧な最適化は難しい場面もあるということでした。
表示領域をブラウザ側が判断できないこと、x記述子で最適化できる場面が限られること、sizesで使える値だと対応できないパターンもあることなどがその原因です。
とはいえ、ある程度のパターンは出てくると思うので、srcsetとsizesの仕様を理解していれば、多くの場面で必要十分な最適化はできそうです。

loading=lazy状態でsizes=autoにするとブラウザ側が画像サイズを取得できるようになるようですが、案件で使えるのはまだまだ先になりそうです。

chot Inc. tech blog

Discussion

MeguriMeguri

解説がとても分かりやすかったです。
個人的に、画像最適化に関しては常に悩ましい問題であり、こうした技術をうまく活用することで、より効率的にページのパフォーマンスを向上させることができると感じました。ありがとうございます。