mizdra's blog

ぽよぐらみんぐ

画像による Layout Shift が無くなる Web がやって来る

はじめに

Web では古来より <img> タグを用いて画像を読み込んでいました. しかし <img> タグにはアスペクト比に関する情報を埋め込むための属性が用意されていません. そのため, ブラウザが画像をネットワークから fetch して読み込みが完了するまで, レスポンシブな img 要素の寸法を決定できず, ページにガタツキ (Layout Shift) が生じる問題がありました.

この問題を解決するため以前より, アスペクト比を埋め込むための新たな属性の導入が提案されていました. しかし最近議論に動きがあり, 既存の属性を利用する方法が提案され, ブラウザに実装され始めています. ここでは問題の背景, 提案と議論の変遷, そして開発者が取るべき対応について紹介します.

img タグと Layout Shift

Web ページにおいて, できるだけ早く意味のあるものをユーザに表示することはとても重要です. ページの表示の遅れはユーザに不快感を与え, ページから離脱する可能性を高めます. そのためブラウザは初期のページの表示に不要なリソースの読み込みを遅延させ, できるだけ早く意味のあるページを表示します.

例えば画像はテキストと比較し, 非常に容量の大きいリソースです. HTML ファイルのダウンロードは数十ミリ秒でできても png ファイルの場合は数秒, 3G のような低速回線では数十秒掛かるかもしれません. そこでブラウザは画像のダウンロードを待たずにページのレンダリングを開始します. img 要素は初回のレンダリングでは 0x0 で描画され, ダウンロードが完了したら正しい寸法でレンダリングし直されます.

しかしこれには画像のダウンロード前後でページにガタツキ, いわゆる Layout Shift が発生するという問題があります. 試しに以下のサンプルページをタブで開いて, リロードして見て下さい. はじめに ページタイトル, パラグラフ とだけ書かれたページがまず表示され, ページタイトル, パラグラフ の間に画像が表示されます. この画像のダウンロード前後に発生するズレが Layout Shift です. *1.

See the Pen oNjaQVr by mizdra (@mizdra) on CodePen.

こうした問題を回避するため, img タグの width / height 属性や CSS の width / height プロパティで img 要素の寸法を指定することが推奨されています. これにより, ブラウザは画像をダウンロードする前に余白を確保し, ダウンロードされたらその余白に画像を描画できるようになります.

See the Pen oNjaQVr by mizdra (@mizdra) on CodePen.

しかし, レスポンシブな img タグの場合は一筋縄ではいきません. というのも, img タグで HTML の width / height 属性と CSS の width / height プロパティを同時に使用した場合, img タグの寸法の算出にはCSSプロパティのみが利用されるからです. 例えば以下のページではHTML属性で width="200" height="100" を指定していますが, 実際に img タグの寸法の算出に利用されるのはCSSプロパティの width: 100%; height: auto; です. そのため, img 要素は初回のレンダリングでは 400x0 で描画され, ダウンロードが完了したら 400x200 でレンダリングし直され, Layout Shift が発生してしまいます.

<div>
  <h1>ページタイトル</h1>
  <img
    class="image"
    src="https://placehold.jp/200x100.png"
    width="200"
    height="100"
    alt="画像"
  />
  <p>パラグラフ</p>
</div>
div {
  box-sizing: border-box;
  border: 4px solid red;
  width: 400px;
}
.image {
  box-sizing: border-box;
  border: 4px solid #333;
  width: 100%;
  height: auto;
}

intrinsicsize 属性

この問題の根本的なレスポンシブな img 要素の寸法はその画像のアスペクト比が分からないと計算できないにも関わらず, 画像をダウンロードする以外にアスペクト比を知る手段が無いことでした. 裏を返せば, ブラウザが画像をダウンロードする前にアスペクト比を知る手段を提供すれば問題を解決できるということです. そこに着目し, 提案されたのが intrinsicsize 属性です.

github.com

googlechrome.github.io

intrinsicsize はブラウザに画像のオリジナルの寸法を無視し, この属性で指定された寸法であると見せかけるよう指示する属性です *2. ブラウザはこの属性で指定された値を画像のオリジナルの寸法としてレイアウトの計算を行います. これにより, ブラウザは画像のダウンロード前に属性値から画像の寸法 (およびアスペクト比) を算出し, レスポンシブな画像であっても適切な余白を確保できるようになります.

例えば200x100の画像を読み込む img タグには以下のように intrinsicsize="200x100" と記述すると, Layout Shift の発生を抑えつつ レスポンシブに画像を表示することができます.

<div>
  <h1>ページタイトル</h1>
  <img
    class="image"
    src="https://placehold.jp/200x100.png"
    intrinsicsize="200x100"
    alt="画像"
  />
  <p>パラグラフ</p>
</div>
div {
  box-sizing: border-box;
  border: 4px solid red;
  width: 400px;
}
.image {
  box-sizing: border-box;
  border: 4px solid #333;
  width: 100%;
  height: auto;
}

intrinsicsize 属性の問題点

しかし, intrinsicsize 属性には 2 つの問題点があります. 1 つは画像のオリジナルの寸法を取得する API (naturalWidth / naturalHeight) に影響を与えてしまうことです. これは属性の仕様上妥当な挙動ではあるのですが, そうした API に依存しているコードの挙動を変えてしまう恐れがあります. もう 1 つは width / height 属性と名前が紛らわしいことです. intrinsicsize 属性は画像オリジナルの寸法を意味する属性ですが, 歴史的には width / height 属性が画像オリジナルの寸法を表していたはずです.

width / height 属性による解決策

そこで intrinsicsize 属性の代替として提案されたのが WICG/intrinsicsize-attribute#16 (以下, 本提案と呼びます) です.

github.com

本提案は CSS により上書きされ無視されていた HTMLの width / height 属性をアスペクトの算出に利用します. intrinsicsize 属性のように画像オリジナルの寸法を指定すると, それを元にブラウザが画像のアスペクト比を算出し, レイアウトを計算します.

Issue ではこれは以下の CSS がブラウザのデフォルトのスタイルシートに追加されるものとみなせると紹介しています.

img {
  aspect-ratio: attr(width) / attr(height);
}

実際には上記の CSS は現在の仕様やブラウザの実装では利用できない機能 *3 *4 を使っており, 本提案の仕様とも厳密には異なっています (詳しくは後述). しかし本提案の概念を説明するための擬似コードとしては良くできているため, 本記事でもこの擬似コードを使って説明することにします.

例えば, この擬似コードからは以下のような特徴が分かります.

  • アスペクト比は既存の width / height 属性から算出されます
    • 古来の Web から存在する慣習に沿うだけで, Layout Shift を抑えられます
    • セマンティクス的にも優れています
  • アスペクト比の事前算出を指示するような特別なマークを付ける必要はありません
    • 上記の CSS はブラウザのデフォルトのスタイルシートに追加されるため, 全ての img 要素に自動的に適用されます
  • 画像のオリジナルの寸法を取得する API に一切影響を与えません
    • ただアスペクト比が width / height 属性から算出されるだけで, 画像のオリジナルの寸法を取得する API に影響はありません
  • 馴染みのある機能や既に標準化団体で議論されている機能から成り立っています
    • aspect-ratio プロパティや attr() 関数, width / height 属性を組み合わせているだけです
    • intrinsicsize のような新たな属性の導入は一切ありません

つまり, 馴染みのある機能などを元に intrinsicsize 属性で問題とされていたことを解決しつつ, Layout Shift を解消している訳です. めっちゃすごい. すごくないですか? 僕はすごいと思いました. とはいえ本提案がすごいのはこれだけに留まりません. Breaking Changes や前方互換性といった面でも優れた仕様を持つ提案となっているのです. どういうことか詳しく見ていきましょう.

Breaking Changes

intrinsicsize 属性と提案とは異なり, 本提案では新規に属性を追加せず, 既存の属性をアスペクト比の算出に利用します. そのため, width / height 属性を使っている要素の挙動が変わる可能性があります. つまり, Breaking Changes が発生する可能性があります. 本提案の導入により予期せぬ表示の崩れや挙動が発生する可能性があることを考えると, どのような Breaking Changes が発生するか知っておくことは重要です.

しかしそれほど心配する必要はありません. 本提案は画像がレスポンシブで表示される場合に, 属性を余白の寸法の決定に利用するものです. つまり, width="200" height="100" のように固定値で要素の寸法を指定している場合や, width="100%" height="100%" のように親要素の寸法から要素の寸法が分かる場合の挙動は変わりません. これらは元からダウンロード前に余白の寸法を知ることができており, Layout Shift も発生していません. Breaking Changes はありません.

また, 本提案は CSS プロパティで上書きされ, 無意味になっていた HTML 属性をアスペクト比の算出に利用するものです. そのため, どちらか片方を指定している場合の挙動は変わりません. CSS には width: 100%; height: auto; が指定されているものの, HTML には width 属性も height 属性もない, といった場合の挙動は変わりません. このケースでは本提案であっても Layout Shift は発生しますが, Layouyt Shift 自体は元より発生しています. Breaking Changes はありません.

ではどういう時に Breaking Changes が発生するのでしょうか. 1 つは width / height 属性に正しいアスペクト比が設定されていて, CSS でレスポンシブに画像を表示しようとしている時です. この場合, ブラウザは width / height 属性を元にアスペクト比を算出し, 余白の寸法の計算に用います. よって, このケースでは本提案により Layout Shift が発生しなくなります. これは Breaking Changes です.

しかし, 元々発生していた Layout Shift が, 本提案により発生しなくなるのでこれは良い Breaking Changes と言えるでしょう. そもそもこれが本提案の目的ですからね. とはいえ画像の読込中の HTMLImageElement.height に依存して何かをしているページでは, この Breaking Changes の影響を受ける可能性があります. 例えば, width / height が 0 であることを利用して画像が読込中かどうかを判定しているページは, この提案の導入により壊れてしまうでしょう. とは言っても画像の読込中の width / height に依存しているコードはそうそうないはず*5で, 代替手段が存在する場合もある *6 ので, Breaking Changes の影響は僅かなはずです.

もう 1 つ, Breaking Changes が起こるケースがあります. それは width / height が不正なアスペクト比で, width / height プロパティに auto が指定されている時です.

See the Pen oNjaQVr by mizdra (@mizdra) on CodePen.

例えば, 上記のようなページがある時, ブラウザは以下のようにページをレンダリングします.

  • 従来
    1. 400x0 の余白を確保した状態でページが表示されます (HTML 属性が無視されるため)
    2. 画像がダウンロードされると, 400x200 で画像が表示されます (画像からアスペクト比が算出されるため)
  • 本提案
    1. 400x400 の余白を確保した状態でページが表示されます (HTML 属性から間違ったアスペクト比を算出して利用するため)
    2. 画像がダウンロードされると, 400x400 で画像が表示されます (前段の間違ったアスペクト比を再利用するため)

1, 2 どちらの段階でも Breaking Changes が発生していますが, 1 は元から壊れていたものの壊れ方が変わっただけで, 多くの場合問題にはならないはずです. 一方 2 は従来は正しい寸法で表示されていた画像が本提案では間違った寸法で表示されてしまっています. これは良くない Breaking Changes です. 不正なアスペクト比が width / height で指定されている箇所はそうないとは思いますが, Breaking Changes が起きないことに越したことはないでしょう.

そこで本提案では 1 の段階では aspect-ratio: attr(width) / attr(height); に基づきアスペクト比を計算し, 2 の段階では画像オリジナルの寸法からアスペクト比を計算するよう拡張が施されています. これが先程チラッと言っていた擬似コードと挙動が異なる部分です. この工夫のおかげで width / height が不正なアスペクト比であっても, Breaking Changes が発生しないようになっています.

前方互換性

しかし, 実際にこのテクニックを使用するにあたって気になってくるのは前方互換性でしょう. ページを訪問するユーザの環境は様々であり, 新しいブラウザを使用する人もいれば, 古いブラウザ (IE11 など) を使用する人もいます. そのため古いブラウザで本提案を使用したページが壊れないか (前方互換性はあるのか) という点が重要になってきます.

これは Web に限らない話ですが, 前方互換性を保ったまま機能追加を行うには古い環境で無視される方法で実装するのが常套手段です. 例えば intrinsicsize 属性は新規に属性を追加する方式を採用しています. ブラウザは未知の属性を無視するようになっているため, 古いブラウザでは単に無視され, 前方互換性が保たれています.

一方, 本提案は古いブラウザにも実装されている属性を利用します. これは一見すると前方互換性がないかのように見えますが, そうではありません. というのも, 本提案は HTML 属性と CSS プロパティ両方を指定してはじめて機能します. 両方が指定されている場合はブラウザは CSS プロパティの値を実際の img 要素のレンダリングに使うため, 古いブラウザでは HTML 属性は無視されます. そのため本提案は前方互換性が保たれており, 古いブラウザのアクセスがあるページでも安心して本提案を使用できます.

標準化と実装

2019/10 に WHATWG が本提案を承認し, 標準化しています.

また, Chrome, Edge, Firefox, Safari 全てのメジャーブラウザで実装が始まっています. Safari については Safari TP 99+で実験的機能として実装されている状態ですが, その他のブラウザについては安定化され (Chrome 79+, Edge 79+, Firefox 69+), 既に Stable で利用できる状態になっています.

開発者が取るべき対応

Breaking Changes の影響を確認する

本提案は既にブラウザに実装されています. つまり, 上記で説明したような Breaking Changes が既に発生しています. そのためまずはあなたの関わっている Web サイトを見て, Breaking Changes の影響を受けていないか確認すると良いでしょう. とはいえ上記で説明したようにレイアウト上の Breaking Changes は僅かです. あったとしても元からあった Layout Shift が無くなるか, 元からガタついている箇所のガタつき方が変わるだけです. 大きな影響を受けるのは JS で HTMLImageElement#widthHTMLImageElement#getAttribute('width') を利用している箇所くらいです. TypeScriptを使用しているなら, Find all references のような機能で当該のコードを探すと良いでしょう.

新規の img タグは width/height 属性を付けて書く

いきなり既存のコードを見ていって Layout Shift を解消するのも良いですが, まずは新規に書く img タグで Layout Shift が発生しないよう書くよう心がけることが重要です.

これからはレスポンシブな img タグを以下のように書くことが推奨されます.

<style>
  div {
    width: 800px;
  }
  img {
    width: 100%;
    height: auto;
  }
</style>
<div>
  <img
    alt="バナー"
    src="https://placehold.jp/200x100.png"
    width="200"
    height="100"
  />
</div>

レスポンシブではない場合でも, 以下のように常に width / height 属性を記述するスタイルでコードを統一するのも良いかもしれません.

<div>
  <img
    alt="バナー"
    src="https://placehold.jp/200x100.png"
    width="200"
    height="100"
    style="width: 100px; height: 50px;"
  />
</div>

これは厳格な規約であり賛否両論あるとは思いますが, Linter などで問題を検知しやすいといった利点があります. 既存のプロジェクトへの導入は難しいかもしれませんが, 1 からプロジェクトを立ち上げる場合はこの規約を採用するのも良いでしょう.

実際に JSX 内の width / height 属性が設定されていない img 要素を警告する ESLint rules を作ってみたので, 良かったら使ってみて下さい.

github.com

既存の Layout Shift を解消する

気が向いたら既存の Layout Shift を解消してみましょう. 既存のコード全てを見て直すのは大変なので, ページにアクセスした直後にユーザから見える領域 (Above the Fold) など重要なところから対応するのがオススメです.

intrinsicsize 属性のその後

intrinsicsize 属性の提案は放棄されました. 実験的に実装されていた Chrome Canary 本体からも既に実装が削除されています. 今後は width / height 属性を使用しましょう.

bugs.chromium.org

まとめ

  • 画像による Layout Shift を回避する仕組みが標準化された
  • 新たな属性を使わず既存の属性を用いて対応でき, セマンティクス的にも優れている
  • 今まで意味の無かった属性を利用することで, 前方互換性がほぼ保たれている
  • 発生する Breaking Changes はごく僅かになるよう, aspect-ratio の考えを拡張している
  • 既にいくつかのブラウザで導入され始めており, すぐにでも試すことができる

参考

*1:DevTools の Network タブから Disable cache を ON に, Throttling を Fast 3G にして意図的に回線速度を遅くすると分かりやすいです

*2:https://developer.mozilla.org/ja/docs/Web/HTML/Element/img#attr-intrinsicsize より

*3:aspect-ratioCSS Box Sizing Module Level 4 で提案されているドラフト段階のプロパティです.

*4:attr() 関数の content プロパティ以外での利用は実験的であり, 多くのブラウザではサポートされていません.

*5:一般に, 取得したいのは画像が読み込まれた後の要素の寸法であり, 壊れた余白の寸法ではないはずです.

*6:画像が読込中かどうかを判断するのであれば onload が使えます.

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.