ブラウザごとに挙動が異なるimg
要素のsrcset
属性およびsizes
属性と、Retinaディスプレーの問題
自分の写真をまとめるためのページを作っていて気がついたのだが、img
要素におけるsrcset
とsizes
属性の挙動はブラウザによって異なっている。Safariの実装が最も遅れていて、Firefoxが一番仕様に忠実に実装されている。Chromeは近いうちに仕様通りの動作になるようだ。
さらに、Retina(HiDPI)ディスプレーでsizes
属性を使うと、ブラウザが少し予想外の挙動をすることもあるので、ついでにメモを残す。
この記事の内容は以下のバージョンのブラウザに基づく。
- Apple Safari Version 15.5 (17613.2.7.1.8)
- Google Chrome Version 102.0.5005.115 (Official Build) (x86_64)
- Firefox 101.0.1 (64-bit)
Safariはsizes
属性内でcalc()
しか使用できない
Safari(Webkit)にはバグがあって、sizes
属性内でサイズを指定する部分(source-size-value
)がcalc()
にしか対応していない。仕様的にはmath functionに対応している必要があるので、min()
やmax()
も使えないといけないのだが、現在の実装はcalc()
決め打ちになっている。
Webkitのバグトラッカーには該当バグのチケットが存在するものの、2018年から放置されている。
min()
が使えないと「左右幅(100vw)、もしくは上下幅(100vh)の小さい方を基準に画像を選択する」といったことが出来ないので不便である。
Chrome(blink)はsizes_math_function_parser.cc
に実装があって、calc()
はもちろんのこと、min()
、max()
、clamp()
などに対応している。Firefoxでも問題なく全ての関数を使用することができる。
<!-- Safariでもcalc()は使える -->
<img sizes="(max-width: 1280px) calc(100vw - 2rem)" ...>
<!-- Safariだとmin()を使った以下の例は動作しない -->
<img sizes="(max-width: 1280px) min(100vw, calc(100vh * 1.5))" ...>
ChromeやSafariはsizes
属性のメディアクエリーがand
にしか対応していない
Chrome(blink)やSafari(WebKit)のsizes
属性のパーサーは、Media Queries Level 4を完全には実装していない。
sizes属性のパース方法を見ると、クエリー部分は正確にはメディアクエリー(<media-query>
)ではなくて、Media Queries Level 4の<media-condition>
として定義されている。
<media-condition>
の定義をみると()
のネストやor
の使用ができるはずなのだが、ChromeやSafariではまだ対応しておらず、Media Query Level 3時代から存在している、and
のみ使うことができる。
<!-- ChromeとSafariが唯一対応しているのはこの構文 -->
<img sizes="(min-width: 100px) and (max-width: 1000px) 200px" ...>
<!-- ChromeとSafariはorが使えないので以下の条件はエラーになる -->
<img sizes="(min-width: 100px) or (max-width: 1000px) 200px" ...>
<!-- ChromeとSafariは()のネストが出来ないのでエラーになる -->
<img sizes="((min-width: 100px) and (max-width: 1000px)) 200px" ...>
Firefoxはこの部分の実装では先行しており、()
のネストもor
も問題なく使用できる。Chromeもすでに実装自体は存在しており、バージョン104からはor
や()
のネストに対応する。バージョン104はすでにDev Channelにリリースされているので、正式版でも遠くない未来に使用できるようになると思われる。
macOSではRetina(HiDPI)ディスプレイにおけるCSS Pixel Ratioが2
で固定されている
MacBookなどでは、内蔵ディスプレイ上でウェブブラウザを使用すると、ディスプレイの設定をどのように変えても、CSS Pixel Ratioが常に2
になる。これにより、ブラウザがsrcset
内の画像から予想以上に大きな画像を選択することがある。
例えば、MacBook Pro (16-inch, 2019)のハードウェア的な画面解像度は3072 x 1920となっている。Pixel Ratioが2
ということは、CSSにおける仮想的なピクセル数は、ハードウェアのピクセル数を2で割って、1536 x 960となる。
しかし、OSデフォルトの解像度設定は1792 x 1120となっており、1536 x 960より一回り大きい。さらに、最大解像度を選ぶと2048 x 1280にまでなる。1792と3072の割合はおおよそ1.7、2048と3072だと1.5で、これらの値がそれぞれの解像度の(計算上の)Pixel Ratioになる。しかし、ChromeもSafariもFirefoxも、OSの解像度設定に関わらず、Pixel Ratioは常に2
に設定されている。
これによって問題になるのが、srcset
属性内の画像選択である。
sizes
属性内で、例えば100vw
が選択されたとする(sizes="100vw"
)。OSのディスプレイ設定が1536 x 960になっていて、ブラウザのウィンドウが最大化されている場合(つまり100vw
= 1536px
)、ブラウザは1536px
をPixel Ratioに基づいて2倍して、3072w
に一番近い画像を探そうとする。ディスプレイの物理的なピクセル数も3072px
なので、妥当な処理に思われる。
一方で、OSのディスプレイ設定が2048 x 1280になっている場合、ブラウザは同様に100vw
= 2048px
を2倍して4096w
の画像を探しに行くことになる。ディスプレイの物理的なピクセル数を超えた画像を探そうとするので、ある意味では予想以上に巨大な画像を選択する可能性がある。もしCSS Pixel Ratioが可変で、解像度設定が2048 x 1280の場合は1.5
に設定されるのであれば、2048px * 1.5
から3072w
が導かれるので、こちらのほうが効率的に思えなくもない。
この挙動がバグかというとそんなことはなくて、どうやらmacOS内部では、常に解像度の2倍で描写を行っているらしい。つまり、OSの解像度が2048 x 1280に設定されている場合、内部的には4096 x 2560の描写領域が存在しており、一度この解像度で描かれたものが、物理的な解像度である3072 x 1920にダウンスケールされているようだ。実際に、画面のスクリーンショットを撮ると、設定解像度の2倍の大きさの画像が生成される。内部的には4096px
の幅があるので、上の例で言えば、3072w
よりも4096w
の画像を選択するのは、理屈の上では妥当だと言える。