srcsetsizes

エリック・ポーティス
鈴木丈 訳

パート1:メディア・クエリのどこがまずいのか?

そう、もし君がウェブサイトを作っている時代が1993年2月23日から2010年5月25日の間だったら、画像の扱いなんてチョロかったね! それはこんなふうに単純だった。

穴の大きさを巻き尺で測る豆男
「画像」と書かれたブロックをサイズに合わせて切り出す豆男
ブロックをハンマーで穴にはめ込む豆男
グリンピースの缶を手に満面の笑みを浮かべる豆男

ときおり聡明なる預言者が荒野から現れては、この手法に潜む問題について深遠な真実を説くこともあった。それでもこのやり方は、20年もの間、ウェブ・デザイナーを生業とするものたちに受け入れられてきた。

しかし、時代は変わる。

、イーサン・マーコットがある記事を書いた、スティーヴ・ジョブズはある携帯電話を発表した。突如として「フルード(流動的)」で「レティナ(高精細)」な画像が重要になった。そしてそれ以来、そこらじゅうから歯ぎしりが聞こえてくる。

フルードとかレティナとかレスポンシブとかいった画像を実装することになったとき、僕らはまずどうするだろうか。直感的に、レスポンシブ・レイアウトで使うのと同じ道具に手を伸ばすんじゃないかな。そう、メディア・クエリだ!

3つのサイズのブロックに張り紙をする豆男。「小さい穴にはこれ」「中くらいの穴にはこれ」「大きい穴にはこれ」

ブラウザーは、まだ読み込んでないウェブサイトについてはなんにも知らないけど、自分たちが描画をおこなう内部の環境については、つねに把握している。ビューポートのサイズや、ユーザーの画面の解像度や、そういったこと。メディア・クエリの狙いは、ウェブ・ディベロッパーが特定の環境に向けてなにかできるようにしよう、というものだ。もしビューポートの幅が1,000px以上なら、サイドバーを左側に表示せよ。そうでなければ、そいつをメインのコンテンツの下に。もしユーザーの画面がレティナなら大きな画像を使え。そうでなければ小さなやつ。

カンタンカンタン。

フォークとナイフを手にグリンピースをほおばる豆男

でも残念なことに、レスポンシブ画像では話が違ってくる。多くの場合、実際に画像のソースを取ってくるのにメディア・クエリを使うと「クソまずいこと」になるんだ。

グリンピースをほおばったままびっくりする豆男。「へっ?」

メディア・クエリを使ってレスポンシブ画像のソースを取ってくるとまずいことになるのはなぜか、その理由をちょっと探ってみよう。まず、ほとんどのデザイナーは、ページのレイアウトをレスポンシブに変化させるとき、1つの変数(ビューポートの幅)をもとにすることに慣れきってる。でもレスポンシブ画像1を扱うとなると、3つもの変数がからんでくるんだ。

つまり、メディア・クエリがややこしくなるってこと。

この3つがわかれば、問題の解決はどうってことない。ひと組のソースがあったら、その中で寸法が描画サイズ×画面密度より大きくて、かついちばん小さなやつを選べばいい。

だがしかし! 残念ながら描画サイズってやつは突き止めるのにちょっと手こずるんだ。ウェブ・ディベロッパーはそいつを知ることができない。なぜなら、画像のサイズを固定せずフレキシブルにすると、画像は伸び縮みするので、レスポンシブ・レイアウトでは描画サイズはあらゆる可能性が考えられる。そしてびっくりするかもしれないけど、ブラウザーが画像の読み込みをはじめるとき、ブラウザーも描画サイズをまだ知らないんだ。描画サイズが決まるのはそのページのCSS次第なんだけど、CSSが解析されるのは、画像の読み込みがはじまったずっとあとなんだ。

メディア・クエリを画像ソースに当てはめると、この描画サイズがわからないという問題をうまく避けられるように見える。メディア・クエリによって描画サイズは次の2つから求められるようになり……

そして製作者は、メディア・クエリでビューポートの寸法と画面密度だけを指定すればいい。そのほかの全部について、いくつかのたくさんのカンタンなややこしい計算を済ませたあとでね。

どんな計算かって? ちょっとためしてみよう。

学校の机で、電卓と鉛筆を手に準備万端の豆男

(注意。僕はシンプルに書こうと努めてるけど、これから親愛なる読者諸君のお目にかける例は、メディア・クエリの計算の過程がどれだけ退屈かつ間違いやすいかを示すためだけにあるんだ。だからもしそのことが飲み込めたと思ったら、すぐにパート2に飛んでもぜんぜんかまわないよ。)

手元に同じ画像の3つのバージョンがあるとしよう。

そして、そのうちの1つを取り出し、フレキシブルなグリッドの中で読み込みたいとする。グリッドのカラムは、はじめは1つだけど、ビューポートが大きければ3つに切り替わる。こんなふうに

さらに、1xと2xのデバイスピクセル比をサポートしたい。

さてメディア・クエリをどうやって組み立てるか? 最初から見ていこう。

large.jpgは本当に必要なときにだけ読み込まれるべきだ。つまりsmall.jpgmedium.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があれば!
「sizes」と「srcset」の間にかかる虹と豆男。満面の笑みを浮かべ、両手を広げている

これを詳しく見る前に、確認しておきたいことが3つある。

まず第一に、これらを実装しているブラウザーはまだない。見通しは明るい2けど、仕様はまだ流動的だ(画像のサイズが流動的なのといっしょ)。だから使うのはちょっと待ってほしい。今はまだ動かないけど、もうすぐ使えるようになる。

ふたつめ。かつて、srcsetと呼ばれるレスポンシブ画像の提案があった。僕たちが話題にしている新しい提案も、同じくsrcsetと呼ばれる属性をもとにしている。古いsrcsetも新しいsrcsetも、カンマ区切りのURLのリストでwディスクリプターを使うけど、それぞれのwまったく違う意味なんだ! 古いwはメディア・クエリのショートハンドで、ビューポートの幅をあらわしてた。一方、新しいwファイルの幅をあらわす。僕らはこれから新しいwについて見ていくので、今のところは、『メン・イン・ブラック』の記憶消去装置みたいなやつを使って、srcsetwについて知ってることをぜんぶ忘れてほしい。

ピカッ

忘れたかな? よし。

みっつめは、以前の<picture>仕様を期待を込めて追っかけてたひとへのお知らせ。新しい<picture>仕様でも、メディア・クエリによるソースの切り替えと、ソースURLでの解像度ディスクリプターは有効だ。もしアート・ディレクションしたり固定サイズの画像を解像度によって出し分けたりしてるなら、間違いなくこれらの機能を使うことになるだろう。でもシンプルに画像を伸び縮みさせるだけなら、ここで紹介する新しいツールが使える。

オーケー。これできれいさっぱり、準備が整った。さっきの例に戻って、今度はsrcsetsizesを使ってみよう。

確認しとくと、僕たちが用意した画像には3つのバージョンがある。

そして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>要素が見当たらない、って気づいたかもしれない。srcsetsizes属性は<img>要素にも組み込むことができるんだ。この例のように、シンプルで、「アート・ディレクション」も「画像フォーマットの切り替え」もいらないようなとき、レスポンシブ画像のマークアップには僕らの古い友人である<img>が使えるし、そうしたほうがいい。

おなじみの<img>に、新しい属性。ひとつずつ見ていこう。

src="small.jpg"

おっと、これはちっとも新しくなかったね! このsrcはフォールバックで、srcsetsizesを理解できないブラウザーでも、今までと同じように画像を読み込むためのものなんだ。次!

srcset="large.jpg 1024w,
        medium.jpg 640w,
        small.jpg 320w"

こいつもほとんど説明いらないよね。srcsetは、利用可能な画像のURLをカンマ区切りのリストで受け取る。それぞれの画像の幅はwディスクリプターで指定する。もし画像を1024 × 768で「ウェブ用に保存」したなら、その画像はsrcset内で1024wと指定すればいい。簡単。

「中くらいの穴にはこれ」から「中くらいのブロック」に貼り替える豆男

指定してるのは幅だけ、ってとこに注意。なんで高さも指定しないのかって? このレイアウトでの画像は幅で制御されてる。その幅はCSSで明示されてるけど、高さは指定されてない。実際のレスポンシブ画像のほとんども幅によって決まるので、仕様では幅だけを扱うことによってシンプルさを保とうとしてるんだ。

将来的には、ファイルの高さもhディスクリプターで指定できたほうがいい理由がいくつかあるけど(個人的にも、それは素晴らしいことだと思う)、今のところはまだ。

そして注意しておきたいのは、srcsetの中のソースに、1x/2xといった解像度ディスクリプターをwディスクリプターのかわりに指定することもできるけど、1x/2xw混ぜて使わないこと。こいつらを同じsrcsetの中で使っちゃダメ。ゼッタイ。

「2x」と「640w」の貼り紙を一緒にしたばかりに雷に打たれる豆男

オーケー、これがsrcsetw

あとブラウザーがソースを選ぶために必要なのは、レイアウトの中で画像がどんなサイズで描画されるかだけ。そのためには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のスマートウォッチが登場したとしても、srcsetsizesならカバーできる。

まだある! この解決策はブラウザーに選択の幅をもたらす。ソースと結びついたメディア・クエリは真か偽かのいずれかの結果になり、もし真なら、ブラウザーはそのソースを読み込まなきゃいけない。でもsizessrcsetはそこまで頑固じゃない。仕様では、通信が遅かったり高くついたりするとき、小さいソースを読み込むオプションが認められている。

「どうやらすべてうまくいくように思えるね」と君は言い、ゆっくりとうなずきながら、条件分岐的アプローチよりも宣言的アプローチのほうに利点がある、と納得しはじめる。「でもちょっと待って……長さってなに?」

長さはあらゆる種類が考えられるよ! 絶対値(99px16em)でも相対値(例に出てきた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" />

カンタン、カンタン。

空っぽのグリンピースの缶