boring stuff

Webサイト上の画像の最適化

webseolighthouse

emmdee@colus.img

lighthouseのPerformanceの数値があまりに悪かったので、その改善のため、まずはトップページのメイン画像や各ページのヘッダーで使用しているサイズの大きな画像の最適化をおこなった。

改善前の状態

トップページでは全画面で表示される画像のため、そもそも表示サイズが大きい上に、タイプ的に解像度は落としたくない画像であるため、非常に大きなサイズのpng画像をCSSのbackground-imageで指定し使用していた。このpng画像自体はそれなりに最適化を行ったものだが、そもそも横幅が2560pxもあるため画像サイズは1.4MBもあった。そのため、特にモバイルなど画面サイズが小さなデバイスに対しては無駄に大きな画像を配信している状態であった。 そこで、ブラウザの画面サイズに合わせて画像を出し分けることから考えはじめた。

画像の出し分け

img要素とCSSのbackground-imageの画像の出し分け方については、下記のqiitaの記事がよくまとまっていてとても参考になった。

簡単にまとめると、

  • img要素ではsrcset属性とsizes属性を使いビューポート幅ごとに出し分けが可能
  • さらにpicture要素を使うと、異なる形式の画像の出し分けが可能。ブラウザが対応している形式のものを表示させることができる
  • background-imageではCSSのメディアクエリーを使い出し分けが可能

といった感じだろうか。

webp

picture要素では複数の画像のフォーマットを簡単に指定できるため、webpを使いやすい。
webpはGoogleが開発しているオープンな画像フォーマットで、Googleがjpegと比較して25-34%、pngと比較して28%小さくなると主張しているものだ。当初はChromeのみがサポートしている状態だったが、現状ではSafari以外のモダンブラウザが全てサポートしている。とはいえ特に日本ではSafariのシェアが大きく、webpのカバー率はまだ大きいとは言えないが、今回の画像のようなサイズの大きな画像では、小さくなるとされる30%程度はそれなりの容量であり、使用を検討してみる価値は十分あるだろう。

picture要素での実装

picture要素の中に様々な条件と共にsource要素を複数書き、最後にデフォルトの画像をimg要素で書く形である。
今回の例ではmedia属性を使い、ビューポート幅568px、768px、980px、1300pxを境に切り分け、srcset属性でデバイスピクセル比を指定し、retinaかそうでないかを切り分けている。 また、type属性でwebpを指定することで、webpに対応しているブラウザかどうかを切り分けている。

<picture>
  <source
    media="(max-width: 567.98px)"    srcset="image-w568.webp, image-w1136.webp 1.5x"
    type="image/webp"
  >
  <source
    media="(max-width: 567.98px)"    srcset="image-w568.jpg, image-w1136.jpg 1.5x"
  >
  <source
    media="(min-width: 568px)"
    srcset="image-w768.webp, image-w1536.webp 1.5x"
    type="image/webp"
  >
  <source
    media="(min-width: 568px)"
    srcset="image-w768.jpg, image-w1536.jpg 1.5x"
  >
  <source
    media="(min-width: 768px)"
    srcset="image-w980.webp, image-w1960.webp 1.5x"
    type="image/webp"
  >
  <source
    media="(min-width: 768px)"
    srcset="image-w980.jpg, image-w1960.jpg 1.5x"
  >
  <source
    media="(min-width: 980px)"
    srcset="image-w1300.webp, image-w2560.webp 1.5x"
    type="image/webp"
  >
  <source
    media="(min-width: 980px)"
    srcset="image-w1300.jpg, image-w2560.jpg 1.5x"
  >
  <source
    media="(min-width:1300px)"
    srcset="image-w2560.webp"
    type="image/webp"
  >
  <source
    media="(min-width:1300px)"
    srcset="image-w2560.jpg"
  >
  <img    src="image-w1300.jpg"    loading="lazy"    decoding="async"    width="1300" height="702"    data-object-fit="cover"  ></picture>

3行目と8行目のmedia="(max-width: 567.98px)"はCSS的に考えると不要ではないかと考えがちだが、picture要素では必要だ。
CSSではメディアクエリが併記されていると最後に書かれているものが優先されるため、最初のものには使用を限定するような記載は必要ない。だが、picture要素では、複数の画像リソースの中から、ブラウザが状況に応じて画像を1つ読み込み表示するという仕様なので、どの画像を読み込むのかはある程度ブラウザ任せとなってしまう。そのため条件を限定しないものを書いてしまうとそれが選択される可能性がある。実際に上記で3行目と8行目のmedia="(max-width: 567.98px)"の指定を外していたところ、Chromeでは4行目に指定した画像を拡大して表示してしまった。
47-53行目のimg要素だが、現状のブラウザではこのsrc属性に指定されたデフォルト画像を使用するのはIEだけと考えてよい。だが、src属性以外の属性は意外に重要で、source要素の画像を読み込むIE以外のモダンブラウザでも、img要素のsrc以外の属性で指定したことが表示に反映される。
49行目のloading="lazy"は遅延読込を指定する属性で、各ブラウザで実装が進んでいるが、ieとsafariは未対応だ。
50行目のdecoding="async"は画像の非同期デコードを指定する属性で、対応しているブラウザでは他のコンテンツの表示が遅れないように、読み込んだ画像が非同期でデコードされる。ie以外のモダンブラウザは対応済みである。
51行目のwidth="" height=""loading="lazy"を指定する場合に記述が推奨される属性であるが、そうでない場合でも最近は画像のアスペクト比を埋め込むための属性として使用され、記述することでLayout Shiftを減らすことが可能な場合がある。

画像による Layout Shift が無くなる Web がやって来る - mizdra's blog

data-object-fit="cover"はIEなどのobject-fit: coverに対応していないブラウザ向けのpollyfillのための記述で、この記述と下記のJavascriptで対応させることができる。

CSSのbackground-imageでの実装

.hdr {
  background-image: url("image-w568.jpg");
  background-position: 50% 50%;
  background-repeat: no-repeat;
  background-size: cover;
}
@media (min-resolution: 144dpi) {
  .hdr {background-image: url("image-w1136.jpg");}
@media (min-width: 568px) {
  .hdr {background-image: url("image-w768.jpg");}
}
@media (min-width: 568px) and (min-resolution: 144dpi) {
  .hdr {background-image: url("image-w1536.jpg");}
}
@media (min-width: 768px) {
  .hdr {background-image: url("image-w980.jpg");}
}
@media (min-width: 768px) and (min-resolution: 144dpi) {
  .hdr {background-image: url("image-w1960.jpg");}
}
@media (min-width: 980px) {
  .hdr {background-image: url("image-w1300.jpg");}
}
@media (min-width: 980px) and (min-resolution: 144dpi) {
  .hdr {background-image: url("image-w2560.jpg");}
}
@media (min-width: 1300px) {
  .hdr {background-image: url("image-w2560.jpg");}
}

メディアクエリのresolutionを使い出し分けを行う。resolutionは2xのようなデバイスピクセル比も使えるが、IEが対応していないのでdpiを使う方が無難だ。
dpiは
dpi = dppx * 96
の式で簡単に求められる。下記がよく使う数値の対応表だ。

x(dppx)11.523
dpi96144192288

また、Safariはresolutionに対応していない。上記の例では意図的に記載を省略したが、autoprefixerを使うと-webkit-min-device-pixel-ratio: 1.5のようなSafariが対応している書式を出力してくれる。
例えば下記は

@media (min-width: 980px) and (min-resolution: 144dpi) {
  .hdr {background-image: url("image-w2560.jpg");}
}

下記のように変換される。

@media
  (min-width: 980px) and (-webkit-min-device-pixel-ratio: 1.5),
  (min-width: 980px) and (min-resolution: 144dpi) {
    .hdr {background-image: url("image-w2560.jpg");}
  }

webpを使いたい場合は、CSSだけでは対応できないため、ModernizrのようなJavascriptを使う必要がある。
すでにModernizrを使用しているような環境であれば、それを使えばよいが、このためだけに導入するのは少し大げさなので、シンプルにwebpに対応しているかだけの判定を行う、下記のスクリプトがお勧めだ。

webpに対応しているブラウザであればbodyclass="webp"が追加される。
CSSは下記のようになる。

@media (min-width: 980px) and (min-resolution: 144dpi) {
  .hdr {background-image: url("image-w2560.jpg");}
  body.webp .hdr {background-image: url("image-w2560.webp");}
}

最適化後

さて上記のような改善を行った結果だが、体感ではサイトの読み込み時間はかなり改善し、明らかな差があった。早速、lighthouseでPerformanceの数値を確認してみたが、……
残念ながら数値的には明らかな改善はしていなかった。lighthouseでは画像以外にもさまざまな点で問題が指摘されているので、他の箇所も引き続き改善していきたい。