srcset と sizes
パート 1: メディア・クエリのどこがまずいのか?
そう、もし君がウェブサイトを作っている時代が 1993 年 2 月 23 日 から 2010 年 5 月 25 日 の間だったら、画像の扱いなんてチョロかったね! それはこんなふうに単純だった。
- 幅の固定されたレイアウトをにらみつける
- 画像がきっかり何ピクセルかを測る—その画像はあらゆるユーザーの画面で変わらないスペースを占めることになる
- Photoshop のエンジンをかける
- 画像をさっき測ったとおりのサイズで「ウェブ用に保存」する
- それを
<img>タグでマークアップする - グラスにビールを注ぎ (または新鮮なグリンピースの缶を開け)、仕事がうまくいったことを祝う
ときおり聡明なる預言者が荒野から現れては、この手法に潜む問題について 深遠な真実 を説くこともあった。それでもこのやり方は、20 年もの間、ウェブ・デザイナーを生業とするものたちに受け入れられてきた。
しかし、時代は変わる。
4 年前、イーサン・マーコットが ある記事を書いた。その 13 日後、スティーヴ・ジョブズは ある携帯電話 を発表した。突如として 「フルード (流動的)」で「レティナ (高精細)」な画像が重要になった。そしてそれ以来、そこらじゅうから歯ぎしりが聞こえてくる。
フルードとかレティナとかレスポンシブとかいった画像を実装することになったとき、僕らはまずどうするだろうか。直感的に、レスポンシブ・レイアウトで使うのと同じ道具に手を伸ばすんじゃないかな。そう、メディア・クエリだ!
ブラウザーは、まだ読み込んでないウェブサイトについてはなんにも知らないけど、自分たちが描画をおこなう内部の環境については、つねに把握している。ビューポートのサイズや、ユーザーの画面の解像度や、そういったこと。メディア・クエリの狙いは、ウェブ・ディベロッパーが特定の環境に向けてなにかできるようにしよう、というものだ。もしビューポートの幅が 1,000px 以上なら、サイドバーを左側に表示せよ。そうでなければ、そいつをメインのコンテンツの下に。もしユーザーの画面がレティナなら大きな画像を使え。そうでなければ小さなやつ。
カンタンカンタン。
でも残念なことに、レスポンシブ画像では話が違ってくる。多くの場合、実際に画像のソースを取ってくるのにメディア・クエリを使うと「クソまずいこと」になるんだ。
メディア・クエリを使ってレスポンシブ画像のソースを取ってくるとまずいことになるのはなぜか、その理由をちょっと探ってみよう。まず、ほとんどのデザイナーは、ページのレイアウトをレスポンシブに変化させるとき、1 つの変数 (ビューポートの幅) をもとにすることに慣れきってる。でもレスポンシブ画像1を扱うとなると、3 つもの変数がからんでくるんだ。
- レイアウトにおける画像の (CSS ピクセルでの) 描画サイズ
- 画面密度
- サイズの異なる画像ファイルそれぞれの寸法
つまり、メディア・クエリがややこしくなるってこと。
この 3 つがわかれば、問題の解決はどうってことない。ひと組のソースがあったら、その中で寸法が 描画サイズ × 画面密度 より大きくて、かついちばん小さなやつを選べばいい。
だがしかし! 残念ながら 描画サイズ ってやつは突き止めるのにちょっと手こずるんだ。ウェブ・ディベロッパーはそいつを知ることができない。なぜなら、画像のサイズを固定せずフレキシブルにすると、画像は伸び縮みするので、レスポンシブ・レイアウトでは 描画サイズ はあらゆる可能性が考えられる。そしてびっくりするかもしれないけど、ブラウザーが画像の読み込みをはじめるとき、ブラウザーも 描画サイズ をまだ知らないんだ。描画サイズ が決まるのはそのページの CSS 次第なんだけど、CSS が解析されるのは、画像の読み込みがはじまったずっとあとなんだ。
メディア・クエリを画像ソースに当てはめると、この 描画サイズ がわからないという問題をうまく避けられ避けられるように見える。メディア・クエリによって 描画サイズ は次の 2 つから求められるようになり…
- ビューポートの寸法
- ビューポートに対する画像の相対的なサイズ
そして製作者は、メディア・クエリでビューポートの寸法と画面密度だけを指定すればいい。そのほかの全部について、いくつかのたくさんのカンタンなややこしい計算を済ませたあとでね。
どんな計算かって? ちょっとためしてみよう。
(注意。僕はシンプルに書こうと努めてるけど、これから親愛なる読者諸君のお目にかける例は、メディア・クエリの計算の過程がどれだけ退屈かつ間違いやすいかを示すためだけにあるんだ。だからもしそのことが飲み込めたと思ったら、すぐに パート 2 に飛んでもぜんぜんかまわないよ。)
手元に同じ画像の 3 つのバージョンがあるとしよう。
large.jpg(1024 x 768)medium.jpg(640 x 480)small.jpg(320 x 240)
そして、そのうちの 1 つを取り出し、フレキシブルなグリッドの中で読み込みたいとする。グリッドのカラムは、はじめは 1 つだけど、ビューポートが大きければ 3 つに切り替わる。こんなふうに。
さらに、1x と 2x の デバイスピクセル比 をサポートしたい。
さてメディア・クエリをどうやって組み立てるか? 最初から見ていこう。
large.jpg は本当に必要なときにだけ読み込まれるべきだ。つまり small.jpg と medium.jpg がどちらも小さすぎるときだけ。もっと正確に言うと、次の式が成り立つときだけ、large.jpg が読み込まれてほしい。
描画される幅 × 画面密度
> 次に小さいファイルの幅
僕たちの例では、描画される幅は単純にビューポートの幅に対するパーセンテージだ。したがって、
描画される幅 =
ビューポートに対する画像の相対的な幅 ×
ビューポートの幅
次に小さいファイルは medium.jpg なので、
次に小さいファイルの幅 = 640px
こいつらをひとまとめにすると次の不等式になる。
ビューポートに対する画像の相対的な幅 ×
ビューポートの幅 ×
画面密度
> 640px
ビューポートの幅 を中心にすると次のようにも書ける。
ビューポートの幅 >
640px ÷
( ビューポートに対する画像の相対的な幅 ×
画面密度 )
そしてここからメディア・クエリを組み立てるには、ビューポートに対する画像の相対的な幅と画面密度がとる可能性のある値すべてについて、それぞれ対応するビューポートの幅を求めなきゃいけない。
ビューポートに対する画像の相対的な幅は次の 2 つのうちどちらかだ。ブレイクポイント (36em) に届く前なら 100vw、それ以降なら 33.3vw。
画面密度については…なんというか、たくさんの可能性が考えられるんだけど、僕たちがサポートする device-pixel-ratio は 1x と 2x だけと決めた。
ビューポートに対する画像の相対的な幅の可能性が 2 つに、画面密度の可能性が 2 つ。これらをかけ合わせると 4 つのシナリオを考えなきゃいけないってことになる。ひとつずつ見ていこう。
1x でブレイクポイント以下の場合
ここでのブレイクポイントは 36em なので、次のことはわかってる。
ビューポートの幅 < 36em
「ビューポートに対する画像の相対的な幅 = 100vw」と「画面密度 = 1x」を、さっき考えた不等式に入れてみよう。
ビューポートの幅 >
640px ÷ ( 100vw × 1x ) = 640px = 40em
この 2 つを合わせると、ありえない結果になる。
36em > ビューポートの幅 > 40em
つまり僕たちはこのシナリオは捨ててしまっていい—1x でレイアウトがシングル・カラムのとき、large.jpg が必要になることはない。
2x でブレイクポイント以下の場合
まずはさっきと同じ。
ビューポートの幅 < 36em
でも今度は 2x に当てはめる。
ビューポートの幅 >
640px ÷ ( 100vw × 2x ) = 320px = 20em
この 2 つを組み合わせると…
36em > ビューポートの幅 > 20em
つまり、2x の画面で large.jpg を読み込んでほしいのは、ビューポートがこの範囲内だった場合ということになる。
1x でブレイクポイント以上の場合
今回はブレイクポイント以上なので、こう。
ビューポートの幅 > 36em
そして 1x の画面では 3 カラム・レイアウトなので、こう。
ビューポートの幅 >
640px ÷ ( 33.3vw × 1x ) = 1920px = 120em
ビューポートが 120em より大きいならもちろん 36em より大きいわけだから、36em の方は忘れちゃっていい。1x の画面で large.jpg を読み込みたいのは、次の場合ということになる。
ビューポートの幅 > 120em
よし、次で最後だ!
2x でブレイクポイント以上の場合
ビューポートの幅 > 36em
…そして…
ビューポートの幅 >
640px ÷ ( 33.3vw × 2x ) = 960px = 60em
…2x の画面で large.jpg を読み込むのは、この場合。
ビューポートの幅 > 60em
さあ、こいつらをメディア・クエリでひとまとめにしよう。
( (min-device-pixel-ratio: 1.5) and (min-width: 20.001em) and (max-width: 35.999em) ) or
( (max-device-pixel-ratio: 1.5) and (min-width: 120.001em) ) or
( (min-device-pixel-ratio: 1.5) and (min-width: 60.001em) )
これと同じ計算を medium.jpg 用に繰り返すのは、読者の練習問題としておこう。
こうして出来上がったのが、<picture> の最初の提案にのっとったこんなマークアップ。
<picture>
<source src="large.jpg"
media="( (min-device-pixel-ratio: 1.5) and (min-width: 20.001em) and (max-width: 35.999em) ) or
( (max-device-pixel-ratio: 1.5) and (min-width: 120.001em) ) or
( (min-device-pixel-ratio: 1.5) and (min-width: 60.001em) )" />
<source src="medium.jpg"
media="( (max-device-pixel-ratio: 1.5) and (min-width: 20.001em) and (max-width: 35.999em) ) or
( (max-device-pixel-ratio: 1.5) and (min-width: 60.001em) ) or
( (min-device-pixel-ratio: 1.5) and (min-width: 10.001em) )" />
<source src="small.jpg" />
<!-- fallback -->
<img src="small.jpg" alt="A rad wolf" />
</picture>
うっ、頭痛が…!
なにしろこのマークアップの山は、device-pixel-ratio が 2 より大きいか 1 より小さい場合をサポートしてないし、2 と 1 の間の値にしてもサポートは不十分。もし device-pixel-ratio をより広くサポートしようとしたら、考えなきゃいけないシナリオもぐんと増える。
そしてこのマークアップの最悪なところは、そこに含まれる値を 1 つでも変更しようとしたときにあきらかになる。ソース画像のサイズ、サポートするデバイスの解像度、または画像のサイズに関わるレイアウトのアスペクト比—これらを変更するたび、僕たちはあの計算をぜんぶ、やり直さいないといけない。
さあ、パート 2 へ急ごう。
パート 2: srcset + sizes = 最高!
どうやらメディア・クエリはこの仕事には向いてないらしい。さて、どうしよう?
ここで、さっき見たレスポンシブ画像の基本となる変数のリストに戻って、それらがいつ変化し、誰が何を知っているのかを考えてみよう。
| 変数 | 製作者がコードを書くとき | ブラウザーがページを読み込むとき |
|---|---|---|
| ビューポートの寸法 | 知らない | 知ってる |
| ビューポートに対する画像の相対的な幅 | 知ってる | 知らない |
| 画面密度 | 知らない | 知ってる |
| ソース・ファイルの寸法 | 知ってる | 知らない |
一方が「知ってる」のとき、もう一方は必ず「知らない」である点に注目! 製作者とブラウザーが知ってることは異なり、互いにおぎない合ってる。我らは鍵の神、彼らは門の神。僕らのパワーをひとつに合わせれば…
このギャップをどう埋めるか?
メディア・クエリは災害対策の詰め合わせみたいなものだ。僕たちはブラウザーにこう話しかける。「なあ、僕はビューポートがどのくらいの大きさになるかわからないんだけど、でももしこのくらいの大きさなら、このファイルを使ってほしい。もしもっと大きければ、こっち。あと、こっちのやつは画面がレティナだった場合に使うけど、でもレイアウトが 3 カラムに切り替わったらそれじゃなくて…」。僕らは様々な可能性についてのラベルをファイルに貼っていく。そのラベルに書いてある内容は、ブラウザーは「知ってる」ことだけど、コードを書いてる僕らは「知らない」ことだ。
でもさっき見たとおり、実際にこれをやるのはとても骨が折れる。
じゃあ、もしこの状態をひっくり返したらどうだろう?
ブラウザーに対してごちゃごちゃした災害対策を提供するかわりに、シンプルにブラウザーが知らないことを教えてやったら? つまり、ビューポートに対する画像の相対的なサイズと、ソース・ファイルの寸法を。僕らはこのどちらも知ってる。もしこの知ってることをブラウザーと共有できたら、ブラウザーはソースを選ぶのに必要なことがすべて手に入ることになるんじゃない?
だよね! 実際、<picture> 仕様の最新にしてもっとも偉大なる草稿 の、sizes 属性と、srcset の中の w ディスクリプターは、まさにそのためにあるんだ。さっきの表をもう一度見てみよう。
| 変数 | 製作者がコードを書くとき | ブラウザーがページを読み込むとき |
|---|---|---|
| ビューポートの寸法 | 知らない | 知ってる |
| ビューポートに対する画像の相対的な幅 | 知ってる | sizes があれば! |
| 画面密度 | 知らない | 知ってる |
| ソース・ファイルの寸法 | 知ってる | srcset があれば! |
これを詳しく見る前に、確認しておきたいことが 3 つある。
まず第一に、これらを実装しているブラウザーはまだない。見通しは明るい2けど、仕様はまだ流動的だ (画像のサイズが流動的なのといっしょ)。だから使うのはちょっと待ってほしい。今はまだ動かないけど、もうすぐ使えるようになる。
ふたつめ。かつて、srcset と呼ばれるレスポンシブ画像の提案があった。僕たちが話題にしている新しい提案も、同じく srcset と呼ばれる属性をもとにしている。古い srcset も新しい srcset も、カンマ区切りの URL のリストで w ディスクリプターを使うけど、それぞれの w はまったく違う意味なんだ! 古い w はメディア・クエリのショートハンドで、ビューポートの幅をあらわしてた。一方、新しい w はファイルの幅をあらわす。僕らはこれから新しい w について見ていくので、今のところは、『メン・イン・ブラック』の記憶消去装置みたいなやつを使って、srcset と w について知ってることをぜんぶ忘れてほしい。
忘れたかな? よし。
みっつめは、以前の <picture> 仕様を期待を込めて追っかけてたひとへのお知らせ。新しい <picture> 仕様でも、メディア・クエリによるソースの切り替えと、ソース URL での解像度ディスクリプターは有効だ。もし アート・ディレクション したり 固定サイズの画像を解像度によって出し分け たりしてるなら、間違いなくこれらの機能を使うことになるだろう。でもシンプルに画像を伸び縮みさせるだけなら、ここで紹介する新しいツールが使える。
オーケー。これできれいさっぱり、準備が整った。さっきの例 に戻って、今度は srcset と sizes を使ってみよう。
確認しとくと、僕たちが用意した画像には 3 つのバージョンがある。
large.jpg(1024 x 768)medium.jpg(640 x 480)small.jpg(320 x 240)
そして 36em のブレイクポイントでグリッドが 1 カラムから 3 カラムに切り替わる。
マークアップはこう。
<img src="small.jpg"
srcset="large.jpg 1024w,
medium.jpg 640w,
small.jpg 320w"
sizes="(min-width: 36em) 33.3vw,
100vw"
alt="A rad wolf" />
“picture” 仕様 をもとにしてるのに <picture> 要素が見当たらない、って気づいたかもしれない。srcset と sizes 属性は <img> 要素にも組み込むことができるんだ。この例のように、シンプルで、「アート・ディレクション」も「画像フォーマットの切り替え」もいらないようなとき、レスポンシブ画像のマークアップには僕らの古い友人である <img> が使えるし、そうしたほうがいい。
おなじみの <img> に、新しい属性。ひとつずつ見ていこう。
src="small.jpg"
おっと、これはちっとも新しくなかったね! この src はフォールバックで、srcset と sizes を理解できないブラウザーでも、今までと同じように画像を読み込むためのものなんだ。次!
srcset="large.jpg 1024w,
medium.jpg 640w,
small.jpg 320w"
こいつもほとんど説明いらないよね。srcset は、利用可能な画像の URL をカンマ区切りのリストで受け取る。それぞれの画像の幅は w ディスクリプターで指定する。もし画像を 1024 × 768 で「ウェブ用に保存」したなら、その画像は srcset 内で 1024w と指定すればいい。簡単。
指定してるのは幅だけ、ってとこに注意。なんで高さも指定しないのかって? このレイアウトでの画像は幅で制御されてる。その幅は CSS で明示されてるけど、高さは指定されてない。実際のレスポンシブ画像のほとんども幅によって決まるので、仕様では幅だけを扱うことによってシンプルさを保とうとしてるんだ。
将来的には、ファイルの高さも h ディスクリプターで指定できたほうがいい理由が いくつか あるけど (個人的にも、それは素晴らしいことだと思う)、今のところはまだ。
そして注意しておきたいのは、srcset の中のソースに、1x/2x といった解像度ディスクリプターを w ディスクリプターのかわりに指定することもできるけど、1x/2x と w は混ぜて使わないこと。こいつらを同じ srcset の中で使っちゃダメ。ゼッタイ。
オーケー、これが srcset と w。
あとブラウザーがソースを選ぶために必要なのは、レイアウトの中で画像がどんなサイズで描画されるかだけ。そのためには sizes がある。さっきの例を見てみよう。
sizes="(min-width: 36em) 33.3vw,
100vw"
フォーマットはこう。
sizes="[メディア・クエリ] [長さ], [メディア・クエリ] [長さ] ..."
このようにメディア・クエリと長さを組み合わせる。ブラウザーは、マッチするものが見つかるまでメディア・クエリを見ていく。もし見つかれば、そのメディア・クエリとペアになった長さを、ソースを取ってくるパズルの最後のピース—描画される画像の幅、またはビューポートに対する相対的な幅として使う。
「なんだって?」と、君は言うかもしれない。「メディア・クエリ? メディア・クエリはまずいって言ってなかったっけ?!」
僕が言ったのは、メディア・クエリをソースを選ぶメカニズムとして使うのはまずい、ってことなんだ。ここでやってるのはそれとは違う。ブラウザーがそのページの CSS で知ることになるブレイクポイントについて、ほんのちょっとだけ先回りして教えてあげてるんだ (このほんのちょっとの時間が すごく重要!)。最初の例では、レイアウトのたった 1 つのブレイクポイント (36em) のために、いくつものクエリが無駄になってたのを覚えてるかな? 60em、20em、10em—ってのがとっちらかってたよね! その点、sizes のブレイクポイントは、そのページのブレイクポイントをそのまま反映したものになるはず。そしてそれぞれのメディア・クエリに続く長さが、そのメディア・クエリがマッチしたときの画像の幅を指定するんだ。
というわけで、ブラウザーは必要な情報をすべて手に入れた。のろまで、なまけもので、間違ってばかりの僕ら人間が パート 1 でやるはめになったような計算は、あとはブラウザーやってくれる。その間に僕らは、くつろいでグリンピースを食べはじめられる。神の意図されたように。
さらに! メディア・クエリの例では 1x と 2x の画面しかカバーできなかったのを覚えてるかな? こっちのマークアップならどんな device-pixel-ratio でも対応できる。もうどの解像度をサポートするのが適当かと迷う必要はない。たとえ 2016 年に 4.8625x のスマートウォッチが登場したとしても、srcset と sizes ならカバーできる。
まだある! この解決策はブラウザーに選択の幅をもたらす。ソースと結びついたメディア・クエリは真か偽かのいずれかの結果になり、もし真なら、ブラウザーはそのソースを読み込まなきゃいけない。でも sizes と srcset はそこまで頑固じゃない。仕様では、通信が遅かったり高くついたりするとき、小さいソースを読み込むオプションが認められている。
「どうやらすべてうまくいくように思えるね」と君は言い、ゆっくりとうなずきながら、条件分岐的アプローチよりも宣言的アプローチのほうに利点がある、と納得しはじめる。「でもちょっと待って…長さってなに?」
長さはあらゆる種類が考えられるよ! 絶対値 (99px や 16em) でも相対値 (例に出てきた 33.3vw とか) でも。ただ実際のレイアウトでは、ここでの例とは違って、絶対値と相対値が組み合わされてることがたくさんあると思う。そこで、意外にもけっこうサポートされてる calc() 関数 の登場。例の 3 カラムのレイアウトに 12em のサイドバーを追加するとしよう。それには sizes 属性をこう調整すればいい。
sizes="(min-width: 36em) calc(.333 * (100vw - 12em)),
100vw"
できあがり!
「わかったわかった」君は思慮深くそう言い、あごをさすりながら、新しい知識がどっと流れ込んできたことにひどく疲れて (でも同時にわくわくして) いる。「けれども、まだひとつ残ってる—そこにぶら下がってる 100vw はなんだ? メディア・クエリを書き忘れたのか?」
仕様の言葉を借りて言うと、メディア・クエリとペアになっていない長さは「デフォルトの長さ」だ。もしマッチするメディア・クエリがなかったとき、この長さが使われる。つまり、巨大な、ページ全体にまたがる幅のバナー画像なら、マークアップはこんなふうにシンプルになる。
<img src="small.jpg"
srcset="large.jpg 1024w, medium.jpg 640w, small.jpg 320w"
sizes="100vw"
alt="A rad wolf" />
カンタン、カンタン。