srcsetとsizes
パート1:メディア・クエリのどこがまずいのか?
そう、もし君がウェブサイトを作っている時代が1993年2月23日から2010年5月25日の間だったら、画像の扱いなんてチョロかったね! それはこんなふうに単純だった。
- 幅の固定されたレイアウトをにらみつける
- 画像がきっかり何ピクセルかを測る――その画像はあらゆるユーザーの画面で変わらないスペースを占めることになる
- Photoshopのエンジンをかける
- 画像をさっき測ったとおりのサイズで「ウェブ用に保存」する
- それを
<img>
タグでマークアップする - グラスにビールを注ぎ(または新鮮なグリンピースの缶を開け)、仕事がうまくいったことを祝う
ときおり聡明なる預言者が荒野から現れては、この手法に潜む問題について深遠な真実を説くこともあった。それでもこのやり方は、20年もの間、ウェブ・デザイナーを生業とするものたちに受け入れられてきた。
しかし、時代は変わる。
、イーサン・マーコットがある記事を書いた。、スティーヴ・ジョブズはある携帯電話を発表した。突如として「フルード(流動的)」で「レティナ(高精細)」な画像が重要になった。そしてそれ以来、そこらじゅうから歯ぎしりが聞こえてくる。
フルードとかレティナとかレスポンシブとかいった画像を実装することになったとき、僕らはまずどうするだろうか。直感的に、レスポンシブ・レイアウトで使うのと同じ道具に手を伸ばすんじゃないかな。そう、メディア・クエリだ!
ブラウザーは、まだ読み込んでないウェブサイトについてはなんにも知らないけど、自分たちが描画をおこなう内部の環境については、つねに把握している。ビューポートのサイズや、ユーザーの画面の解像度や、そういったこと。メディア・クエリの狙いは、ウェブ・ディベロッパーが特定の環境に向けてなにかできるようにしよう、というものだ。もしビューポートの幅が1,000px以上なら、サイドバーを左側に表示せよ。そうでなければ、そいつをメインのコンテンツの下に。もしユーザーの画面がレティナなら大きな画像を使え。そうでなければ小さなやつ。
カンタンカンタン。
でも残念なことに、レスポンシブ画像では話が違ってくる。多くの場合、実際に画像のソースを取ってくるのにメディア・クエリを使うと「クソまずいこと」になるんだ。
メディア・クエリを使ってレスポンシブ画像のソースを取ってくるとまずいことになるのはなぜか、その理由をちょっと探ってみよう。まず、ほとんどのデザイナーは、ページのレイアウトをレスポンシブに変化させるとき、1つの変数(ビューポートの幅)をもとにすることに慣れきってる。でもレスポンシブ画像1を扱うとなると、3つもの変数がからんでくるんだ。
- レイアウトにおける画像の(CSSピクセルでの)描画サイズ
- 画面密度
- サイズの異なる画像ファイルそれぞれの寸法
つまり、メディア・クエリがややこしくなるってこと。
この3つがわかれば、問題の解決はどうってことない。ひと組のソースがあったら、その中で寸法が描画サイズ×画面密度より大きくて、かついちばん小さなやつを選べばいい。
だがしかし! 残念ながら描画サイズってやつは突き止めるのにちょっと手こずるんだ。ウェブ・ディベロッパーはそいつを知ることができない。なぜなら、画像のサイズを固定せずフレキシブルにすると、画像は伸び縮みするので、レスポンシブ・レイアウトでは描画サイズはあらゆる可能性が考えられる。そしてびっくりするかもしれないけど、ブラウザーが画像の読み込みをはじめるとき、ブラウザーも描画サイズをまだ知らないんだ。描画サイズが決まるのはそのページのCSS次第なんだけど、CSSが解析されるのは、画像の読み込みがはじまったずっとあとなんだ。
メディア・クエリを画像ソースに当てはめると、この描画サイズがわからないという問題をうまく避けられるように見える。メディア・クエリによって描画サイズは次の2つから求められるようになり……
- ビューポートの寸法
- ビューポートに対する画像の相対的なサイズ
そして製作者は、メディア・クエリでビューポートの寸法と画面密度だけを指定すればいい。そのほかの全部について、いくつかのたくさんのカンタンなややこしい計算を済ませたあとでね。
どんな計算かって? ちょっとためしてみよう。
(注意。僕はシンプルに書こうと努めてるけど、これから親愛なる読者諸君のお目にかける例は、メディア・クエリの計算の過程がどれだけ退屈かつ間違いやすいかを示すためだけにあるんだ。だからもしそのことが飲み込めたと思ったら、すぐにパート2に飛んでもぜんぜんかまわないよ。)
手元に同じ画像の3つのバージョンがあるとしよう。
large.jpg
(1024 × 768)medium.jpg
(640 × 480)small.jpg
(320 × 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 × 768)medium.jpg
(640 × 480)small.jpg
(320 × 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" />
カンタン、カンタン。