フロントエンドコーディングでやっておきたい表示速度対策

Web の進化により、フロントエンドコーディングはデザインを再現するだけでは済まなくなりました。

「ふぅ～。デザイン通りにコーディングしてやったぜぇ～:smirk:」
では終われません！

デザインを再現する以外にも注力することがいっぱい増えました…:persevere:

というわけで、フロントエンドコーディングでやっておきたい**表示速度対策**をまとめてみました！

## 次世代画像形式

ページ内に表示する画像は圧縮率の高い、次世代画像形式を利用します。

:::note warn
一般的には、AVIF が一番圧縮率が高いですが、画像によっては、WebP や JPEG、または PNG の方が軽くなる場合があります。
:::

### AVIF

Alliance for Open Media（AOMedia）によって開発された新しい画像形式です。

* 高い圧縮率（WebP よりも高圧縮）
* 可逆圧縮と非可逆圧縮の両保存形式に対応
* 透過処理に対応
* アニメーションに対応
* HDR（High Dynamic Range）に対応

:::note info
HDR（High Dynamic Range）は、従来の SDR（Standard Dynamic Range）に比べてより広い明るさの幅（ダイナミックレンジ）を表現できる表示技術です。「色深度（明暗差の階調）」を増やすことで、黒つぶれや白飛びしない、より自然でリアルな描写が可能になります。
:::

#### 対応ブラウザ

Safari 以外のブラウザであれば表示可能です。
Safari は macOS 13 Ventura **以降**が利用可能となります:expressionless:

|OS|リリース日|
|:-|:-|
|macOS 13 Ventura|2022年10月24日|

:::note info
2024年1月26日にリリースされた Edge 121 で、Edge も利用可能になりました:stuck_out_tongue_closed_eyes:
:::

https://caniuse.com/avif

### WebP

Google によって開発された新しい画像形式です。

* 高い圧縮率
* 可逆圧縮と非可逆圧縮の両保存形式に対応
* 透過処理に対応
* アニメーションに対応
* 対応ブラウザ

#### 対応ブラウザ

Safari 以外のブラウザであれば表示可能です。
Safari は以下 OS **以降**が利用可能となります:expressionless:

|OS|リリース日|
|:-|:-|
|macOS 11 Big Sur|2020年11月13日|
|iOS 14|2020年9月17日|

https://caniuse.com/webp

### 作成方法

JPEG or PNG を AVIF / WebP に変換して作成します。

#### Squoosh

ブラウザ上で AVIF / WebP を作成できます。

https://squoosh.app/

#### WebP Converter

WebP を作成できるアプリケーション。（Win / Mac 対応）

https://anywebp.com/software

#### sharp

Node.js のパッケージ。
AVIF / WebP を作成できます。

https://www.npmjs.com/package/sharp

sharp の利用例を記事にしたので参考にしてみてください。

https://qiita.com/taqumo/items/60de0af9699415150035

### 表示方法

非対応ブラウザも考慮する必要があります。
1つの画像につき、3種類の画像形式（AVIF / WebP / JPEG or PNG）の画像ファイルを作成します。

:::note warn
画像のファイル名は、ファイル名の競合を防ぐため、元画像の拡張子も含めたほうが安全です。
元画像の拡張子を残さない場合、`image.jpg` と `image.png` がある場合に `image.webp` と同じ名前になってしまいます。
:::

作成した画像は以下の優先度で、ブラウザごとに振り分けます。

|優先度|ブラウザ|表示する画像形式|
|:-|:-|:-|
|1|AVIF 対応ブラウザ|AVIF|
|2|AVIF 非対応ブラウザ|WebP|
|3|AVIF / WebP 非対応ブラウザ|JPEG、PNG|

画像の振り分けは `picture` 要素、または `.htaccess` で振り分ける方法があります。

#### picture 要素

`picture` 要素を利用して画像を振り分けます。

* `source` 要素に AVIF / WebP 画像を指定
* `img` 要素に AVIF / WebP 非対応ブラウザ用画像を指定

```html:HTML
<picture>
 <!-- 🥇優先順位1 -->
 <source srcset="image.jpg.avif" type="image/avif">
 <!-- 🥈優先順位2 -->
 <source srcset="image.jpg.webp" type="image/webp">
 <!-- 🥉優先順位3 -->
 <img src="image.jpg">
</picture>
```

#### .htaccess

HTML は WebP 非対応ブラウザ用画像を読み込んでおきます。

```html:HTML
<img src="image.jpg">
```

画像の振り分け処理を `.htaccess` に記述します。

:::note warn
画像のファイル名に元画像の拡張子を含むか、含まないかで `.htaccess` の記述が異なります。
:::

```apache:.htaccess
<IfModule mod_rewrite.c>
  RewriteEngine On

  # AVIF の処理
  RewriteCond %{HTTP_ACCEPT} image/avif
  RewriteCond %{REQUEST_URI} (?i)(.*)(\.jpe?g|\.png|\.gif)$
  # 元画像の拡張子を含む
  RewriteCond %{DOCUMENT_ROOT}%1%2.avif -f
  RewriteRule (?i)(.*)(\.jpe?g|\.png|\.gif)$ %1$2\.avif [L,T=image/avif,R]
  # 元画像の拡張子を含まない
  # RewriteCond %{DOCUMENT_ROOT}%1.avif -f
  # RewriteRule (?i)(.*)(\.jpe?g|\.png|\.gif)$ %1\.avif [L,T=image/avif,R]

  # WebP の処理
  RewriteCond %{HTTP_ACCEPT} image/webp
  RewriteCond %{REQUEST_URI} (?i)(.*)(\.jpe?g|\.png|\.gif)$
  # 元画像の拡張子を含む
  RewriteCond %{DOCUMENT_ROOT}%1%2.webp -f
  RewriteRule (?i)(.*)(\.jpe?g|\.png|\.gif)$ %1$2\.webp [L,T=image/webp,R]
  # 元画像の拡張子を含まない
  # RewriteCond %{DOCUMENT_ROOT}%1.webp -f
  # RewriteRule (?i)(.*)(\.jpe?g|\.png|\.gif)$ %1\.webp [L,T=image/webp,R]
</IfModule>

<IfModule mod_mime.c>
  AddType image/webp	.webp
  AddType image/avif	.avif
</IfModule>

<IfModule mod_headers.c>
  Header append Vary Accept env=REDIRECT_accept
</IfModule>
```

### 確認方法

AVIF / WebP の表示確認はブラウザの開発者ツールで行えます。
その他に、Chrome の拡張機能「WebP and AVIF Highlighter」を利用すると簡単に確認できます。

:::note info
個別の画像だけであれば、画像を保存しようとした時に表示される画像名の拡張子で確認できます。
:::

#### 開発者ツール

1. `F12` を押して開発者ツールを表示
2. `ネットワーク` を選択
3. `webp` でフィルタ
4. `タイプ` 列が `octet-stream or webp` になっているか確認する

:::note warn
AVIF のタイプは `avif` ではなく、`octet-stream` と表示されるようです。
:::

![webp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492675/fc4dcd24-c2b0-a0bb-a8b5-155f73593339.png)

#### WebP and AVIF Highlighter

https://chrome.google.com/webstore/detail/webp-and-avif-highlighter/aonapkfkfneahhaonjjpmcabpnbdmojl?hl=JA

WebP and AVIF Highlighter を有効にすると WebP 画像に枠線が表示されます。
線の色は3種類あり、色の違いは以下となります。

|色|意味|
|:-|:-|
|緑|非可逆圧縮|
|ピンク|可逆圧縮|
|赤|アルファチャンネルなど拡張を含む画像|

![webp-and-avif-highlighter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492675/0f94df31-cea6-a72c-2d6f-618ef94d5d58.png)

## リソースヒント

:::note info
リソースヒントは、これから必要になるリソースを事前に読み込んでおくことで、ページ遷移時のパフォーマンスを向上させます。
:::

リソースヒントは以下の4種類あり、ネットワークの流れによってタイミングが異なります。

![resource-hints.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492675/e18031c6-4822-b03d-e672-be745912580a.png)

### dns-prefetch

:::note info
`rel="dns-prefetch"` を指定すると、事前に DNS の名前解決（ドメイン名をIPアドレスに変換）をしておきます。
:::

https://developer.mozilla.org/ja/docs/Web/HTML/Link_types/dns-prefetch

```html:HTML
<link rel="dns-prefetch" href="https://example.com">
```

### preconnect

:::note info
`rel="preconnect"` を指定すると、事前に DNS の名前解決、 TCP の接続まで確立しておきます。
:::

https://developer.mozilla.org/ja/docs/Web/HTML/Link_types/preconnect

Google Fonts の利用コードには `preconnect` が指定されています。

https://fonts.google.com/

```html:HTML
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
```

### prefetch

:::note info
`rel="prefetch"` を指定すると、事前に静的リソースをダウンロードしておきます。
:::

https://developer.mozilla.org/ja/docs/Web/HTML/Link_types/prefetch

```html:HTML
<link rel="prefetch" href="next-page.css" as="style">
<link rel="prefetch" href="next-page.js" as="script">
```

### prerender

:::note info
`rel="prerender"` を指定すると、事前にページを表示できる状態にしておきます。
:::

https://developer.mozilla.org/ja/docs/Glossary/prerender

```html:HTML
<link rel="prerender" href="https://example.com/nextpage/">
```

## プリロード

:::note info
`rel="preload"` を指定すると、リソースを早く読み込むようになり、ページのレンダリングをブロックしません。
:::

https://developer.mozilla.org/ja/docs/Web/HTML/Link_types/preload

以下はよく利用しそうな `as` 属性の値です。
フォントの場合は CORS を有効にするために `crossorigin` 属性も指定します。

|as 属性|内容|
|:-|:-|
|font|フォントファイル|
|image|画像ファイル|
|script|JavaScript ファイル|
|style|CSS ファイル|

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

```html:HTML
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="style.css" as="style">
<link rel="preload" href="script.js" as="script">
```

:::note warn
何でもかんでも preload させるのは逆効果です。<br>ページの表示に関わる主要なリソースを指定しておきましょう。
:::

## img 要素

### width height 属性

:::note info
`width` と `height` を指定すると、ブラウザが画像の幅と高さを直ぐ決定でき、コンテンツの再配置を行うことなく表示できます。
:::

```html:HTML
<img src="image.jpg" width="1000" height="1000">
```

### decoding 属性

:::note info
`decoding="async"` を指定すると、画像を非同期でデコードし、他のコンテンツ表示の遅延を減らせます。
:::

https://developer.mozilla.org/ja/docs/Web/API/HTMLImageElement/decoding

`decoading` 属性は以下の値が指定できます。

|属性値|内容|
|:-|:-|
|auto|既定値。ブラウザが最適なものを決定する|
|sync|他のコンテンツと画像を同期的にデコード|
|async|他のコンテンツと画像を非同期的にデコード|

```html:HTML
<img src="image.jpg" decoding="async">
```

### loading 属性

:::note info
`loading="lazy"` を指定すると、ビューポート内にある画像のみを読み込み、それ以外の画像はビューポート内に入るまで読み込みません。
:::

https://developer.mozilla.org/ja/docs/Web/API/HTMLImageElement/loading

`loading` 属性は以下の値が指定できます。

|属性値|内容|
|:-|:-|
|eager|既定値。画像をビューポート外でもすぐに読み込む|
|lazy|画像を遅延読み込みする|

```html:HTML
<img src="image.jpg" loading="lazy">
```

:::note warn
メインビジュアルなどのファーストビューに表示される画像への指定は不要です。
:::

## レスポンシブイメージ

:::note info
デバイスに応じた最適なサイズ、見た目の画像を表示します。
:::

### 解像度で変更

解像度に応じて画像を変更する場合は、`srcset` 属性に画像のパスと画面の密度（デバイスピクセル比）を指定します。

https://developer.mozilla.org/ja/docs/Web/API/HTMLImageElement/srcset

```html:HTML
<img src="image.jpg" srcset="image.jpg, image@2x.jpg 2x">
```

### アートディレクション

画面幅に応じて画像を変更する場合は、`picture` 要素を利用します。

https://developer.mozilla.org/ja/docs/Web/HTML/Element/picture



```html:HTML
<picture>
  <source media="(max-width: 500px)" srcset="small.jpg">
  <source media="(max-width: 900px)" srcset="medium.jpg">
  <img src="large.jpg" srcset="large.jpg, large@2x.jpg 2x">
</picture>
```

## script 要素

:::note info
`script` 要素に `async` や `defer` 属性を付与することで、HTML パースを中断させないようにします。
:::

https://developer.mozilla.org/ja/docs/Web/HTML/Element/script

`async` と `defer` 属性の違いは以下となります。

|属性|実行の順番|実行のタイミング|
|:-|:-|:-|
|async|ダウンロードが完了した順|ダウンロードが完了した直後|
|defer|コード順|`DOMContentLoaded` イベントの直前|

JavaScript は DOM を操作することがほとんどなので、基本的に `defer` を利用することになると思います。

```html:HTML
<script src="script.js" defer></script>
```

## JavaScript のバンドル

:::note info
複数の JavaScript を1つにまとめることで、リクエスト回数が減り、ページの表示速度向上が期待できます。
:::

バンドルするために Browserify 、 webpack 、Vite を利用することが多いと思います。

https://browserify.org/

https://webpack.js.org/

https://ja.vitejs.dev/

## ミニファイ

:::note info
ミニファイは、HTML、CSS、JavaScript などのコード中の不要な改行やインデントを削除し、ファイルサイズを小さくします。
:::

:::note warn
HTML は、ソースコードを確認することが多いので、ミニファイしないほうが安全です。
:::

プロジェクトにミニファイツールが導入されていない場合は、以下がオススメです。

https://rakko.tools/tools/33/

## gzip 圧縮

:::note info
gzip 圧縮は、HTML、CSS、JavaScriptなどのテキストファイルを圧縮し、ファイルサイズを小さくします。
:::

### Apache の設定方法

gzip 圧縮 の設定内容は `.htaccess` に記述します。
設定方法は以下の2通りあります。

#### mod_deflate

`mod_deflate` は、クライアントからのアクセスによって、サーバがファイルを圧縮します。

:::note warn
サーバーに負荷がかかります。
:::

```apache:.htaccess
SetOutputFilter DEFLATE
# 圧縮しないファイルの拡張子を設定
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|webp|ico)$ no-gzip dont-vary
# 指定したファイル以外をすべてgzip圧縮する
Header append Vary User-Agent env=!dont-vary
```

#### mod_rewrite

`mod_rewrite` は、クライアントからアクセスされた URL を gzip ファイルの URL に書き換えます。

:::note warn
gzip ファイルを作成し、サーバーに設置しておく必要があります。
ファイルを更新した場合は gzip ファイルも更新しないと、古いファイルが読み込まれたままになります。
頻繁に更新する HTML は gzip 圧縮しないほうが安全です。
:::


```apache:.htaccess
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteCond %{HTTP:Accept-Encoding} gzip
RewriteCond %{REQUEST_URI} (\.html|\.css|\.js)$
RewriteCond %{REQUEST_FILENAME}\.gz -s
RewriteRule .* %{REQUEST_URI}.gz [L]
</IfModule>

<IfModule mod_headers.c>
  <FilesMatch "\.html\.gz$">
  ForceType text/html
  AddEncoding x-gzip .gz
  </FilesMatch>

  <FilesMatch "\.css\.gz$">
  ForceType text/css
  AddEncoding x-gzip .gz
  </FilesMatch>

  <FilesMatch "\.js\.gz$">
  ForceType application/x-javascript
  AddEncoding x-gzip .gz
  </FilesMatch>
</IfModule>
```

### 確認方法

gzip 圧縮が有効かどうかはブラウザの開発者ツールで確認できます。

1. `F12` を押して開発者ツールを表示
2. `ネットワーク` を選択
3. gzip 圧縮したファイルを選択
4. レスポンスヘッダー内に `content-encoding: gzip` があるか確認する

![gzip.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492675/d96563c4-cde6-1d2a-c28f-e82cf7b41753.png)

## ブラウザキャッシュ

:::note info
更新頻度の低いファイルはブラウザキャッシュを使って表示させることで、ページの表示速度を早くします。
:::

|ファイル|ブラウザキャッシュの保存期間|
|:-|:-|
|更新頻度低い|長い|
|更新頻度高い|短い|

### Apache の設定方法

ブラウザキャッシュの設定は `.htaccess` に記述します。

:::note info
`Cache-Controlヘッダー` 、`Expiresヘッダー` 2種類の設定方法がありますが、Google は `Cache-Controlヘッダー` を推奨しています。
:::

https://developers.google.com/speed/docs/insights/LeverageBrowserCaching

```apache:.htaccess
<IfModule mod_headers.c>
  # 1時間
  <FilesMatch "\.(html)(\.gz)?$">
  Header set Cache-Control "max-age=3600, public"
  </FilesMatch>

  # 2週間
  <FilesMatch "\.(css|js|gif|jpe?g|png|webp|svg|pdf)(\.gz)?$">
  Header set Cache-Control "max-age=1209600, public"
  </FilesMatch>

  # 1年
  <FilesMatch "\.(ico|woff|woff2)$">
  Header set Cache-Control "max-age=31536000, public"
  </FilesMatch>

  # キャッシュさせたくない場合
  # <FilesMatch "\.(css|js)$">
  # 内容に変更があればキャッシュを使用してはいけないという意味
  # 一切キャッシュを保存しない場合は、no-cache の部分を no-store にする
  # Header set Cache-Control no-cache
  # 古い環境に対応するための設定
  # Header set Pragma no-cache
  # </FilesMatch>
</IfModule>
```

### 確認方法

キャッシュの確認はブラウザの開発者ツールで確認できます。

1. `F12` を押して開発者ツールを表示
2. `ネットワーク` を選択
3. キャッシュの有効期限を設定したファイルを選択
4. レスポンスヘッダー内の `cache-control` に設定した秒数が反映されているか確認する

![cache.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492675/29740548-77c1-bc5d-0272-d384df871b2d.png)

## 関連記事

https://qiita.com/taqumo/items/0c7116517bb2b44b2939

https://qiita.com/taqumo/items/d69522e2f3b95bf02996

https://qiita.com/taqumo/items/76a47a588b896e3c9840