amp-img から学ぶ画像の表示のベストプラクティス

[AMP](https://amp.dev/ja/) は Google が推奨しているウェブコンポーネントフレームワークで、その実装には Web サイトのパフォーマンスを向上させるための知見が詰まっています。
AMP コンポーネントの実装を詳しく見ていくと、 AMP を導入せずにサイトを作る際にも役に立つベストプラクティスを学べるのではないかと思います。
以下では [amp-img](https://amp.dev/ja/documentation/components/amp-img/) コンポーネントに注目して web サイト開発における画像表示の実装について掘り下げます。

## amp-img
amp-img は AMP 対応のサイトで画像を表示する際に、 HTML の img タグの代わりに使用します。
AMP の built-in 要素であるため、 amp-img 専用の js を追加で読む必要はなく、AMP のランタイムを通して自動的に使用できます。

### amp-img の基本の使い方
```html
<amp-img src="/static/sample.jpg" width="1080" height="720" layout="fixed" alt="sample"></amp-img>
```

**参考**

- [ドキュメント：amp-img](https://amp.dev/ja/documentation/components/amp-img/)
- https://github.com/ampproject/amphtml


## 画面の解像度に合わせた画像を表示
HTML の img タグと同様に amp-img タグでも`srcset` 属性や `size`属性を使用することができます。
高解像度の画面（Retina ディスプレイなど）で高解像な画像を配信する場合きれいな画像を表示できるメリットがありますが、低解像度の画面でも必要以上に重い画像を時間をかけて読み込まなくてはなりません。
`srcset` 属性や `size` 属性を使用することで、画面の解像度に合わせて適切な画像を表示することができます。

```html
<amp-img alt="sample"
  src="/static/sample-640.jpg"
  width="640"
  height="400"
  srcset="/static/sample-640.jpg 640w,
          /static/sample-320.jpg 320w"
  sizes="(min-width: 650px) 50vw, 100vw">
</amp-img>
```

## アートディレクション
ブレークポイントの条件に合わせて画像を変更することをアートディレクションと言います。
amp-img では `media` 属性によってブレークポイントを指定して画像自体を切り替えることができます。
`srcset`属性 と `size` 属性による画像の切り替えは、基本的に解像度のみ異なる同一の画像を切り替えることが想定されていますが、`media` 属性の場合は画像のアスペクト比が異なっていても問題ありません。

```html
<amp-img alt="sample"
  media="(max-width: 768px)"
  width="226"
  height="340"
  src="/static/sample-medium.jpg"></amp-img>
<amp-img alt="sample"
  media="(mim-width: 769px)"
  width="450"
  height="340"
  src="/static/sample-small.jpg"></amp-img>
```

通常の HTML タグでアートディレクションを実現する場合、picture タグを使用します。[^1]

```html
<picture>
  <source srcset="/static/sample-medium.jpg" media="(max-width: 768px)">
  <source srcset="/static/sample-small.jpg" media="(mim-width: 769px)">
  <img src="/static/sample-small.jpg" alt="sample">
</picture>
```

**参考**

- [レスポンシブな AMP ページを作成する - amp.dev](https://amp.dev/ja/documentation/guides-and-tutorials/develop/style_and_layout/responsive_design/?format=websites)
- [画像要素 - HTML: HyperText Markup Language | MDN](https://developer.mozilla.org/ja/docs/Web/HTML/Element/picture)

## 遅延ロード

画像の遅延ロードは、画面に表示されていない画像は読み込まずに、画面のスクロールに応じてあとから画像を読み込むことでウェブページの表示を高速化する手法です。
通常の img タグを使用した場合、ページアクセス時にすべての img タグの画像を読み込みます。
これによりページが最低限の操作を受け付けるようになるまでに時間がかかってしまったり、スクロールしない場合に不要な通信が発生したり、といったことが起きます。

amp-img は画面のスクロール位置に応じて、amp-img タグ内に img タグを生成します。この時に初めて画像の取得のための通信が発生するため効率的に画像をロードすることができます。

```html
<amp-img src="/static/sample.jpg" width="1080" height="720" layout="fixed" alt="sample"></amp-img>
```
↓

```html
<amp-img src="/static/sample.jpg" width="1080" height="720" layout="fixed" alt="sample" class="i-amphtml-element i-amphtml-layout-fixed i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="fixed" style="width: 1080px; height: 610px;">
  <img decoding="async" alt="sample" src="/static/sample.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content">
</amp-img>
```

amp-img を使用せずに遅延ロードを行う場合、スクラッチで実装するほか、[lazyload](https://github.com/tuupola/lazyload)などのライブラリの利用や、img タグの loading 属性を使用する方法があります。（ただし、[Safari と IE11 は未対応](https://caniuse.com/mdn-html_elements_img_loading])）

## 非同期で画像をデコード
展開されたコードの img タグを見ると、`decoding="async"`の指定があります。
画像データは通常ファイルサイズを小さくするためにエンコードされているため、ブラウザは画像を表示する際はデータをデコードする必要があります。
img タグに `decoding` 属性によってブラウザに同期/非同期のどちらでデコードするかのヒントを提供でき、全てのモダンブラウザに対応しています。[^1]
[^1]: IEは非対応: [https://caniuse.com/mdn-html_elements_img_decoding](https://caniuse.com/mdn-html_elements_img_decoding)
`decoding` 属性に指定できる値は下記の通りです。


| 値 | 説明 |
|:-:|:-:|
| sync  | 画像を同期的にデコードする |
| async | 画像を非同期でデコードする |
| auto | 優先設定なし(デフォルト値) |

amp-img コンポーネントは、img タグに`decoding="async"`を付与することで、メインスレッドの処理をブロックせずに画像を非同期的にデコードさせています。

**参考**

- https://html.spec.whatwg.org/multipage/images.html#decoding-images
- [画像埋め込み要素 - HTML: HyperText Markup Language | MDN](https://developer.mozilla.org/ja/docs/Web/HTML/Element/img)

## 画像のフォールバック
amp-img では、例えば webp の画像を使用する際に、webp 未対応のブラウザ用に jpg の画像を設定したい場合など、画像の読み込みに失敗した際のフォールバック画像を指定することができます。

```html
<amp-img alt="sample" width="1080" height="720" src="/static/sample.webp">
  <amp-img alt="sample" fallback width="1080" height="720" src="/static/samples.jpg"></amp-img>
</amp-img>
```

amp-img を使用しない場合、picture タグを使用することで実現できます。

```html
<picture>
  <source type="image/webp" srcset="/static/sample.webp" /> // wepb 対応の場合は source タグの画像を提供
  <img src="/static/samples.jpg" width="1080" height="720" alt="sample" />  // wepb 未対応ブラウザで source タグの画像を提供できない場合の代替画像
</picture>
```
**参考**
- [画像要素 - HTML: HyperText Markup Language | MDN](https://developer.mozilla.org/ja/docs/Web/HTML/Element/picture)

## レイアウトシフト対策
Google の提唱する UX 指標である [Core Web Vitals](https://developers-jp.googleblog.com/2020/05/web-vitals.html) の一つに [Cumulative Layout Shift（CLS）](https://web.dev/cls/) があります。
これは予期せぬレイアウトのズレや崩れを独自に指標化し評価しているもので、画像が読み込まれた際のこのレイアウトの移動によってスコアが下がってしまいます。
amp-img コンポーネントは、画面幅によって画像の幅 / 高さが変化しない場合、画像を表示する明示的なサイズを `width` と `height` に指定することで amp-img のインラインスタイルとして展開され、レイアウト時に画像の表示領域が確保されます。[^2]
[^2]:  `layout` の値により、`width` / `height` が必須かどうかは異なります。
画面幅によって画像の表示サイズが変化する場合は、適切なスタイルを付与することによりレイアウトシフトを防いでいます（後述）。

通常の img タグの場合も`width` / `height`属性への値を指定や、CSS で`width` / `height`に具体値を指定することで画像を読み込む前に場所を確保し、レイアウトシフトを防ぐことができます。
2019 年 10 月に WHATWG が img タグの `widht` / `height` 属性に基づいてデフォルトのアスペクト比を設定できる仕様が標準化されました。
そのため現在のモダンブラウザでは、画面の幅によって画像の表示サイズが変化するレスポンシブな表示でも `width` と `height` 利用してレイアウトシフトを防ぐことができます。

例えば、下記のようにな img タグを設置した場合、3 : 2 のアスペクト比でブラウザが表示場所を計算して確保してくれます。

```html
<img src="/static/samples.jpg" width="1080" height="720" alt="sample">
```

```css
img {
  width: 100%;
  height: auto;
}
```

**参考**

- [Optimize Cumulative Layout Shift](https://web.dev/optimize-cls/#images-without-dimensions)
- [画像による Layout Shift が無くなる Web がやって来る - mizdra's blog](https://www.mizdra.net/entry/2020/05/31/192613)

## レスポンシブ対応のスタイル
### layout 属性
amp-img コンポーネントは `layout` 属性を指定することにより、[AMP のレイアウトシステム](https://amp.dev/ja/documentation/guides-and-tutorials/learn/amp-html-layout/?format=websites)を利用できます。
`layout` 属性は AMP コンポーネント共通の属性ですが、コンポーネントによってサポートされる値が異なります。
amp-img コンポーネントは `container` 以外の下記のレイアウトに対応しています。

- fill
- fixed
- fixed-height
- responsive
- flex-item
- intrinsic
- nodisplay

amp-img タグとその子要素の img タグには、`layout` 属性の指定に応じた class が指定されます。
レスポンシブ対応をする際に、ウインドウや親のコンテンツのサイズの変化に伴ってどのように画像の表示を変化させるのかは往々にして悩ましいものです。
そんな時に `layout` 属性による画像を様々な表示パターンとそのスタイルは参考になります。
下記ではそれぞれの `layout` の値を amp-img に指定した場合のスタイルを詳しく見ていきます。
なお、`layout` 属性は amp-img 専用の属性ではなく、AMP コンポーネント共通のものであるため、画像以外のコンテンツにも応用できます。
各 `layout` 値を指定した際のアニメーション画像は[公式ドキュメント](https://amp.dev/ja/documentation/guides-and-tutorials/learn/amp-html-layout/layouts_demonstrated/)からお借りしました。

**参考**

- [レイアウトとメディアクエリ - amp.dev](https://amp.dev/ja/documentation/guides-and-tutorials/develop/style_and_layout/responsive_design/?format=websites#displaying-responsive-images)
- [AMPHTML Layout System - amp.dev](https://amp.dev/ja/documentation/guides-and-tutorials/learn/amp-html-layout/?format=websites)

### layout="fill"
親要素のコンテンツボックス全体を埋めるように拡大縮小されます。
`width`と `height` に指定したアスペクト比が親要素のアスペクト比と合わない場合は引き伸ばされて表示されます。
![fill.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174446/e704180c-df13-7fa6-d9c9-9020b359493b.gif)

```html
<amp-img src="/static/samples.jpg" width="1080" height="720" layout="fill" alt="sample"></amp-img>
```
↓ 画像ロード後

```html
<amp-img src="/static/samples.jpg" layout="fill" alt="sample" class="i-amphtml-element i-amphtml-layout-fill i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="fill" style="--loader-delay-offset:434ms !important;">
  <img decoding="async" alt="sample" src="/static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content">
</amp-img>
```

```css
/* amp-img タグのスタイル */
.i-amphtml-layout-size-defined {
  overflow: hidden!important;
}
.i-amphtml-layout-fixed {
  display: block;
  overflow: hidden!important; /* .i-amphtml-layout-size-defined のスタイルによって打ち消される */
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
}
/* img タグのスタイル */
.i-amphtml-layout-size-defined .i-amphtml-fill-content {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
}
.i-amphtml-replaced-content {
  padding: 0!important;
  border: none!important;
}
.i-amphtml-replaced-content {
  padding: 0!important;
  border: none!important;
}
.i-amphtml-fill-content {
  display: block;
  height: 0;
  max-height: 100%;
  max-width: 100%;
  min-height: 100%;
  min-width: 100%;
  width: 0;
  margin: auto;
}
```
amp-img タグは `.i-amphtml-layout-fixed`に指定されているスタイルにより、`position: absolute;`であるため、直近の`position: static;`以外の `position` の値を持つ親要素の上下左右いっぱいに広がるように表示されます。

img タグは `.i-amphtml-layout-size-defined .i-amphtml-fill-content` に指定されているスタイルにより、直近の`position: static;`以外の `position` の値を持つ親要素、つまり amp-img の上下左右いっぱいに広がるように表示されます。
img が amp-img のサイズいっぱいになる構造は、どの `layout` を指定した場合でも共通しています。
`layout="fill"`の場合は amp-img タグと img タグが共に`position: absolute;`であるため、基準となる親要素に高さを明示的に指定する必要があります。

`.i-amphtml-replaced-content`に指定されているスタイルは、ユーザーエージェントスタイルを打ち消すためのスタイルです。
また、`.i-amphtml-fill-content`に指定されているスタイルは、iOS で iframe をレスポンシブ対応させる際にコンテンツのサイズをコンテナーに収めるための指定のようです。([issue](https://github.com/ampproject/amphtml/issues/11133))
`layout` 属性は他のコンポーネントでも使用されるため、amp-img タグの場合でもこのようなスタイルが適応されます。
これらのスタイルは他の `layout` 値を指定した場合でも登場しますが、今回の内容には直接関係ないため以下では割愛します。

### layout="fixed"
ウインドウのサイズが変化しても画像の大きさは変化せず、`width`と`height`属性に基づいて固定の寸法のまま表示されます。

![fixed.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174446/c02b1a50-1400-82dc-238b-4187dbb7474f.gif)

```html
<amp-img src="/static/samples.jpg" width="1080" height="720" layout="fixed" alt="sample"></amp-img>
```
↓ 画像ロード後

```html
<amp-img src="/static/samples.jpg" width="1080" height="720" layout="fixed" alt="sample" class="i-amphtml-element i-amphtml-layout-fixed i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="fixed" style="width: 1080px; height: 610px; --loader-delay-offset:226ms !important;">
  <img decoding="async" alt="sample" src="/static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content">
</amp-img>
```

```css
/* amp-img タグのスタイル */
element.style {
  width: 1080px;
  height: 610px;
  --loader-delay-offset: 188ms !important;
}
.i-amphtml-layout-size-defined {
  overflow: hidden!important;
}
.i-amphtml-layout-fixed {
  display: inline-block;
  position: relative;
}
```

amp-img タグにはインラインスタイルとして `width` と `height`属性で指定した値が指定されています。
`overflow: hidden;` が指定されているため、画像の表示サイズよりウインドウが小さくなった場合もスクロールは発生せずに画像は見切れて表示されます。

### layout="fixed-height"
高さは固定のまま、幅は使用可能なスペースに合わせて広がります。

![fixed-height.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174446/de1fbd10-286b-3e22-ab6f-2047158f6ddb.gif)

```html
<amp-img src="/static/samples.jpg" height="720" layout="fixed-height" alt="sample"></amp-img>
```
↓ 画像ロード後

```html
<amp-img src="/static/samples.jpg" layout="fixed-height" height="720" alt="sample" class="i-amphtml-element i-amphtml-layout-fixed-height i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="fixed-height" style="height: 720px; --loader-delay-offset:377ms !important;">
  <img decoding="async" alt="sample" src="/static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content">
</amp-img>
```

```css
/* amp-img タグのスタイル */
element.style {
    height: 720px;
}
.i-amphtml-layout-size-defined {
  overflow: hidden!important;
}
.i-amphtml-layout-fixed-height {
  display: block;
  position: relative;
}
```

### layout="responsive"
`width` / `height `属性で指定されたアスペクト比を維持したまま、使用可能なスペースに合わせて拡大・縮小します。

![responsive.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174446/77e9423a-fc8e-a3c2-cfaf-a7638fad44de.gif)

```html
<amp-img src="/static/samples.jpg" width="1080" height="720" layout="responsive" alt="sample"></amp-img>
```

↓ 画像ロード後

```html
<amp-img src="/static/samples.jpg" layout="fixed-height" height="720" alt="sample" class="i-amphtml-element i-amphtml-layout-fixed-height i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="fixed-height" style="--loader-delay-offset:377ms !important;">
  <i-amphtml-sizer slot="i-amphtml-svc" style="padding-top: 66.6667%;"></i-amphtml-sizer>
  <img decoding="async" alt="sample" src="/static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content">
</amp-img>
```

```css
/* amp-img タグのスタイル */
.i-amphtml-layout-size-defined {
    overflow: hidden!important;
}
/* i-amphtml-sizer タグのスタイル */
element.style {
    padding-top: 66.6667%;
}
.i-amphtml-sizer {
  display: block!important;
}  
```

`layout="responsive`の場合、amp-img の直下に img タグに加えて i-amphtml-sizer タグが挿入されます。
amp-img タグの `width` と `height` に指定した値から、画像のアスペクト比は `1080:720 = 3:2` となります。
このアスペクト比を維持した状態で、表示可能な領域内で画像を拡大・縮小させるために、`2 / 3 * 100 ≒ 66.6667%;`を `padding-top` として与えています。
i-amphtml-sizer タグによって親要素の amp-img の大きさが確保され、その amp-img の大きさに合わせて img タグが広がることで、画像のアスペクト比を維持したまま画像が拡大・縮小されます。
この方法であれば、IE11 でもレイアウトシフトを防ぐことができます。

### layout="flex-item"
フレックスボックスの親要素の直下でフレックスアイテムとして画像を扱いたい場合に使用します。
親要素のスタイルと兄弟要素の数によって画像の大きさが変化します。

![flex-item.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174446/30bf48b5-b45b-82f4-2515-5706ffc76795.gif)

```html
<amp-img src="/static/samples.jpg" layout="flex-item" alt="sample"></amp-img>
```
↓ 画像ロード後

```html
<amp-img src="/static/samples.jpg" layout="flex-item" class="i-amphtml-layout-flex-item i-amphtml-layout-size-defined i-amphtml-element i-amphtml-layout" i-amphtml-layout="flex-item" alt="sample">
  <img decoding="async" alt="sample" src="//static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content">
</amp-img>
```

```css
/* amp-img タグのスタイル */
.i-amphtml-layout-flex-item {
  display: block;
  position: relative;
  flex: 1 1 auto;
}
```
`flex: 1 1 auto;` が指定されているため、フレックスボックスの直下に `layout="flex-item"`の amp-img を隣接して配置して横に並べた場合、同じ幅で並びます。
また、兄弟要素が`layout="flex-item"`の要素のみで高さを持たない場合は、親要素のフレックスボックスに高さを明示的に与える必要があります。

### layout="intrinsic"

![intrinsic.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174446/30128829-5a85-cb32-3f4f-958cd35eca2d.gif)

画像自体の本来のサイズか CSS による制限（max-width など）に達するまで、`width` と `height `属性で指定されたアスペクト比を維持したまま使用可能なスペースに合わせて拡大・縮小します。

```html
<amp-img src="/static/samples.jpg" width="1080" height="720" layout="intrinsic" alt="sample"></amp-img>
```
↓ 画像ロード後

```html
<amp-img src="/static/samples.jpg" width="1080" height="720" layout="intrinsic" alt="sample" class="i-amphtml-element i-amphtml-layout-intrinsic i-amphtml-layout-size-defined i-amphtml-layout" i-amphtml-layout="intrinsic" style="--loader-delay-offset:241ms !important;">
  <i-amphtml-sizer class="i-amphtml-sizer" slot="i-amphtml-svc">
    <img alt="" role="presentation" aria-hidden="true" class="i-amphtml-intrinsic-sizer" src="data:image/svg+xml;charset=utf-8,<svg height=&quot;720px&quot; width=&quot;1080px&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; version=&quot;1.1&quot;/>">
  </i-amphtml-sizer>
  <img decoding="async" alt="sample" src="/static/samples.jpg" class="i-amphtml-fill-content i-amphtml-replaced-content">
</amp-img>
```

```css
/* amp-img タグのスタイル */
.i-amphtml-layout-size-defined {
    overflow: hidden!important;
}
.i-amphtml-layout-intrinsic {
  display: inline-block;
  position: relative;
  max-width: 100%;
}
/* i-amphtml-sizer タグのスタイル */
.i-amphtml-layout-intrinsic .i-amphtml-sizer {
  max-width: 100%;
}
i-amphtml-sizer {
  display: block!important;
}
/* i-amphtml-sizer タグ直下の img タグのスタイル */
.i-amphtml-intrinsic-sizer {
  max-width: 100%;
  display: block!important;
}
```
`layout="responsive"`と同様に amp-img の直下に img タグに加えて i-amphtml-sizer タグが挿入されます。
しかし、`layout="intrinsic"`の場合は i-amphtml-sizer の直下にさらにもう一つ img タグが挿入されます。
`layout="responsive"`では、空の i-amphtml-sizer タグに指定された `padding` が画像の表示領域を確保していたのに対し、 `layout="intrinsic"`では i-amphtml-sizer タグの直下の img タグによって表示される透明な svg 画像によって画像の表示領域を確保します。
svg 画像のサイズは amp-img に指定した `width` と `height` 属性によって決まり、img タグであるため画像自体の大きさより大きくなることはありません。

また、i-amphtml-sizer タグ内の img タグには、`role="presentation"` と `aria-hidden="true"` を指定することで、見た目を変えるために使用されている重要な意味を持たない要素であることを明示してあり、アクセシビリティにも考慮されていることがわかります。

### layout="nodisplay"
要素は非表示になり、スペースを占有しません。

```html
<amp-img layout="nodisplay" src="/static/sample.jpg" width="1080" height="720" layout="nodisplay"></amp-img>
```

↓ 画像ロード後

```html
<amp-img layout="nodisplay" src="/static/sample.jpg" width="1080" height="720" class="i-amphtml-layout-nodisplay i-amphtml-element" hidden="" i-amphtml-layout="nodisplay"></amp-img>
```
```css
/* amp-img タグのスタイル */
[hidden] {
    display: none!important;
}
```

AMP コンポーネント共通の `hidden` 属性が付与され、`display: none;`が適用されます。

## まとめ
amp-img を使用しない場合でも`width` や `height`、`decoding` といった属性を適切に指定するなど、パフォーマンス改善のために画像周りで工夫できることが多くあることがわかりました。
amp-img は遅延ロードやフォールバックといった機能を提供してくれるので、改めて便利だと感じました。
完全に AMP 対応したサイトではないとしても画像を便利に扱うために amp-img を使用するのも一つの選択肢だと思いました。
画像の扱いは web サイトのパフォーマンスを低下させる大きな要因になり得るので、先人の知恵を借りながらベストな実装を追いかけていきたいですね。