Webサイトの表示画像をWebPにする方法, 検証

# この記事は何か
最近の業務で、サイト表示画像をWebP化させる取り組みを行なったので、実現方法の流れ, 検証した事項などをシェアします。

# 前段
## WebPとは
Googleが開発した次世代画像フォーマットのことです。
「ウェッピー」と読みます。
圧縮率が高く、画像サイズが軽いという特徴があります。

参考: [WebPリファレンス](https://developers.google.com/speed/webp?hl=ja)

## なぜ表示画像をWebPにするのか
主に以下2点のメリットを享受するため、導入を検討しました。

### 画像描画速度の改善
より軽量な画像のため、ブラウザにダウンロードされる速度が速くなること, 描画速度が速くなることが期待されます。
[弊社サイト](https://our-photo.co/)では、画像を多く扱っているため、画像描画速度を改善することは、ユーザ体験の向上にもつながってきます。

### SEOの改善
画像描画速度の改善は、ページ自体の表示速度改善にもつながります。
ページ表示速度は、検索結果ランキングの要因の1つであると言われています。
例えば、[PageSpeed Insights](https://pagespeed.web.dev/)で表示速度を分析すると、改善できる項目として以下の指摘がされます。

> 次世代フォーマットでの画像の配信
WebP や AVIF などの画像形式は、一般的に PNG や JPEG より圧縮率が高く、ダウンロード時間やデータ消費量を抑えられます。

## WebPの制約
画像をWebP化すれば万事解決ということはありません。
WebPにもいくつかの制約がありますので、理解した上で利用していくことが重要です。
私が調べた限りだと、以下のような制約がありました。

### 対応していないカラーモデル
[CMYKカラーモデル](https://ja.wikipedia.org/wiki/CMYK)には対応していません。
CMYKカラーモデルをWebP形式に変換すると、色がくすんだり、色味が変わったりする恐れがあるため注意が必要です。

### 最大ピクセル数
WebPの最大ピクセル数は`16383 x 16383 ピクセル`です。
これを超える画像の場合、事前にリサイズ等してから、WebPに変換する必要があります。

### 対応していないブラウザ
[caniuse.com](https://caniuse.com/webp)で、WebPのブラウザ対応状況を確認すると、

- IE
- 一部古いバージョンのブラウザ

では、WebPに非対応なことがわかります。

![スクリーンショット 2023-07-08 14.28.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351743/797af77f-e5ca-ba6c-f717-9b676379a9ad.png)

# 画像WebP化実現までの流れ
## WebP化実現に必要なこと
画像をWebP化するためには、大きく分けて以下3つの実装が必要です。

### アップロードされた画像を自動でWebP形式に変換させる
デザイナーやサイト制作者が、表示させたい画像を逐一WebPに変換するのは手間です。
また、WebP化させるための方法や、利用する変換ツールが人によって異なると、画像の品質や信頼性も低下してしまいます。

そのため、「同じ方法 & 自動で」WebP形式に変換させるべく、システム側で、画像をWebP形式に変換する機構を実装する必要があります。

### 既に表示されている画像をWebP形式に変換させる
既に表示されている過去画像は、別途WebP形式に変換させる必要があります。
バッチ処理などを回して一括で変換するのが良いでしょう。

ただし、単純に考えて画像数が倍になる（元画像 & WebP画像の2種類になる）ため、変換対象の画像数が多い場合は画像保持コストや、WebP変換時のコストなどに気を配る必要があります。

### WebP非対応ブラウザでは、WebP以外の形式の画像を表示させる
前述したように、WebPに非対応のブラウザがいくつか存在します。
非対応ブラウザでは画像を表示させない という方針を取らない場合は、画像を出し分ける（非対応ブラウザではJPEGなど他フォーマットで画像を表示させる）などの対策を行う必要があります。

## WebP化実現までの流れ
特にページ数が多いサイトの場合、すべてのページについて、一気に表示画像をWebP化するのは現実的ではありません。
そのため弊社では、以下の3つの観点をページごとに評価し、費用対効果が高いページから順次実施を進めていきました。

- SEO的重要度
    - ログイン前/後どちらの画面か
    - 対象ページの閲覧数
    - 検索ランキング上位に載せたいページかどうか
    - CV（コンバージョン）につながるページかどうか。つながる場合はCV数。
- 対応工数
- 実施に伴う金額面コスト
    - 主に「過去画像WebP化」に伴い発生するコスト（弊社の場合はAWS金額コスト）

# 表示画像をWebPにするための実装方法
## 前提
- 弊社アプリケーションはPHP（Laravelフレームワーク）を用いて実装していますので、記事に記載する実装例も、PHP（Laravel）で実装されたものとなります。
- 一方、登場するライブラリーなどはPHP以外の言語でも使用可能かと思いますので、参考にしていただければと思います。

## WebP化対象画像
今回は、以下のJPEG画像をWebPに変換していきます。
（※私のプロフィール画像です。本検証ではexif情報を残した画像を使っています）
![display-test.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351743/d1086661-ebc7-82e0-5e3f-c0a7e3e7de6a.jpeg)

- 画像形式: JPEGイメージ
- ファイルサイズ: 7.2 MB
- イメージサイズ: 6048 × 4024 ピクセル
- イメージのDPI: 300 ピクセル/インチ
- カラーモデル: RGB
- ColorSyncプロファイル: sRGB IEC61966-2.1

また、JPEG圧縮品質は`85`を指定した上で、WebPへの変換を行います。（[85を指定する理由](https://developers.google.com/speed/docs/insights/OptimizeImages?hl=ja)）

## JPEG → WebP に自動変換する手法
画像をWebP形式に変換する方法はいくつかあります。
ここでは以下を要件とした上で、各方法について検討していきます。

- 【Must】画像の見た目が損なわれないこと
- 【Must】画像のイメージサイズ, DPI, 色空間情報が保持されること
- 【Should】シンプルに実装できる方法であること

### 方法1: ImageMagickを利用した方法
ImageMagickを用いて、画像をWebP形式に変換することができます。
PHPでは、[Imagick](https://www.php.net/manual/ja/intro.imagick.php)という、ImageMagickを利用するための拡張モジュールが存在しており、こちらを利用することとします。
[Imagick::setImageFormat](https://www.php.net/manual/ja/imagick.setimageformat)というメソッドを使うことで、任意の画像をWebP形式に変換することが可能です。

#### 導入例
:::note warn
**導入例の補足**
Alpine Linux環境下での導入方法例を記載しています。
また、ポイントとなるライブラリのみ記載していますので、実際に導入される際は、各開発環境に合わせて適切な導入方法にて実施してください。以降も同様です。
:::
```dockerfile
apk add imagemagick imagemagick-dev && \
  pecl install imagick && \
  docker-php-ext-enable imagick
```

#### 実装例
```php5
// 対象画像
$fileName = 'webp-test.jpg';
$targetFilePath = storage_path($fileName);

// WebP形式に変換
$im = new \Imagick($targetFilePath);
$im->setImageFormat('webp');
$im->setImageCompressionQuality(85);

// 拡張子を.webpに変えて保存
$webpFileName = pathinfo($fileName)['filename'] . '.webp';
$resultFilePath = storage_path($webpFileName);
$im->writeImage($resultFilePath);
```

#### WebP変換後の画像情報
- ファイルサイズ: 653 KB
- イメージサイズ: 6048 × 4024 ピクセル
- イメージのDPI: 300 ピクセル/インチ
- カラーモデル: RGB
- ColorSyncプロファイル: sRGB IEC61966-2.1

WebP変換により、ファイルサイズを大幅に減らすことができました。
その他、各種画像情報も保持できており、問題なさそうです。

### 方法2: GD関数 > imagewebp を利用した方法
GDライブラリの[imagewebp関数](https://www.php.net/manual/ja/function.imagewebp.php)を用いることでも、画像をWebP形式に変換することができます。

#### 導入例
```dockerfile
apk add libpng-dev libjpeg-turbo-dev libwebp-dev && \
  docker-php-ext-configure gd --with-jpeg={DIR} --with-webp={DIR} && \
  docker-php-ext-install gd
```

#### 実装例
```php5
// 対象画像
$fileName = 'webp-test.jpg';
$targetFilePath = storage_path($fileName);

// GDオブジェクトを取得
$gd = imagecreatefromjpeg($targetFilePath);

// 保存先ディレクトリを指定
$fileData = pathinfo($targetFilePath);
$resultFilePath = $fileData['dirname'] . '/' . $fileData['filename'] . '.webp';

// WebP形式に変換
imagewebp($gd, $resultFilePath, 85);
```

#### WebP変換後の画像情報
- ファイルサイズ: 622 KB
- イメージサイズ: 6048 × 4024 ピクセル
- イメージのDPI: 72 ピクセル/インチ
- カラーモデル: RGB
- ColorSyncプロファイル: -

サイズは圧縮できたものの、イメージのDPI値が変わってしまったのと、ColorSyncプロファイル情報が保持されませんでした。

### 方法3: コマンドラインツール cwebp を用いる
画像をWebP形式に変換するコマンドラインツールも、いくつか存在します。
ここでは、[cwebp](https://developers.google.com/speed/webp/docs/cwebp?hl=ja)というコマンドラインツールを、PHPから呼び出す方法を試してみます。

#### 導入例
```dockerfile
apk add libwebp-tools
```

#### 実装例
```php5
// 対象画像
$fileName = 'webp-test.jpg';
$targetFilePath = storage_path($fileName);

// 保存先ディレクトリを指定
$fileData = pathinfo($targetFilePath);
$resultFilePath = $fileData['dirname'] . '/' . $fileData['filename'] . '.webp';

// WebP形式に変換
// -q 85: 圧縮品質85で実施
// -metadata all: メタデータを全て保持
exec(sprintf('cwebp -q 85 -metadata all %s -o %s', $targetFilePath, $resultFilePath));
```

#### WebP変換後の画像情報
- ファイルサイズ: 653 KB
- イメージサイズ: 6048 × 4024 ピクセル
- イメージのDPI: 300 ピクセル/インチ
- カラーモデル: RGB
- ColorSyncプロファイル: sRGB IEC61966-2.1

cwebpを用いた方法でも、ImageMagickと同様、各種の画像情報を保持しつつ、WebP形式に変換することができました。

### 選定
- 方法1:ImageMagickを利用した方法
- 方法3:コマンドラインツールcwebpを用いる

の2つが、今回の要件を満たしています。
弊社では、既存でImageMagickを既に利用していたため、実装工数の観点から、ImageMagickにて変換を行う方法を選択しました。

## WebP非対応ブラウザでは、WebP以外の形式の画像を表示させる手法
[picture要素](https://developer.mozilla.org/ja/docs/Web/HTML/Element/picture)を用いて、

- ブラウザがWebP対応: WebP形式の画像を表示
- ブラウザがWebP非対応: 元画像（jpeg, png...）を表示

という形で出し分けることができます。

#### 実装例
```html
<picture>
    <source srcset="{WebP画像パス}" type="image/webp">
    <img src="{元画像パス}">
 </picture>
```

# 各種検証
表示画像のWebP化を採用するにあたり、問題がないかどうか、検証した事項を書いていきます。

## ブラウザ越しで見た際に、画像の見た目に問題がないかどうか
ブラウザのバージョン, 種類などによっては、画像の見た目（色味, 輪郭のぼやけなど）に変化が生じる可能性を危惧し、検証を行いました。

対象ブラウザについては、PC, スマホ(実機)の両方において、ユーザの利用率が高いデバイスをGAなどで調べて選定するのが良いかと思います。

ここでは、PC（Chrome）, スマホ(Safari)を例にします。

### PC(Chrome)
![スクリーンショット-2023-07-09-11-12-31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351743/efb45e79-8c28-4a7a-bb37-4451bbe7946d.png)

### スマホ(Safari)
![764F067C-D889-4697-9F1E-36DC164784A7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351743/38f4f97a-3bb1-a919-becd-341bb27ac2a8.png)

スマホの方は、表示用に拡大されている関係で写りが若干ボケていますが、JPEG, WebPとで比較すると、見た目に大きな違いはなく問題なさそうです。

## WebP変換処理における負荷(メモリ)
変換処理の過程でサイズの大きな画像を扱うことになるため、メモリ使用量が過多にならないかどうか検証します。処理の前後で、PHPの[memory_get_usage](https://www.php.net/manual/ja/function.memory-get-usage.php), [memory_get_peak_usage](https://www.php.net/manual/ja/function.memory-get-peak-usage) を仕込んで検証していきます。

### コード例
```php5
dump('処理開始前のメモリ使用量(MB): ' . memory_get_usage() / (1024 * 1024));

// 対象画像
$fileName = 'webp-test.jpg';
$targetFilePath = storage_path($fileName);

// WebP形式に変換
$im = new \Imagick($targetFilePath);
$im->setImageFormat('webp');
$im->setImageCompressionQuality(85);

// 拡張子を.webpに変えて保存
$webpFileName = pathinfo($fileName)['filename'] . '.webp';
$resultFilePath = storage_path($webpFileName);
$im->writeImage($resultFilePath);

dump('処理終了時のメモリ使用量(MB): ' . memory_get_usage() / (1024 * 1024));
dump('最大メモリ使用量(MB): ' . memory_get_peak_usage() / (1024 * 1024));
```

### 結果
```
"処理開始前のメモリ使用量(MB): 42.003952026367"
"処理終了時のメモリ使用量(MB): 42.474906921387"
"最大メモリ使用量(MB): 42.581817626953"
```

処理対象の画像サイズは7.2MBでしたが、使用されたメモリ量は`最大メモリ使用量 - 処理開始前のメモリ使用量 = 0.578MB`なので、こちらも問題なさそうです。

## SEO指標の計測
[Lighthouse](https://chrome.google.com/webstore/detail/lighthouse/blipmdconlkpinefehnmjammfjpmpbjk)というツールを利用して、画像をWebPに変換したことにより、サイト評価的に効果が出ているかどうかを確認します。

### JPEG
![スクリーンショット 2023-07-09 11.50.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351743/a2b82955-d5a5-b393-99ee-ff552504dcc2.png)

### WebP
![スクリーンショット 2023-07-09 11.51.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351743/f382dbe4-538b-5e66-3931-048ae2a4905d.png)

今回は検証のため、対象画像のみ表示された簡易なサイトで計測しましたが、

> Serve images in next-gen formats
次世代フォーマットの画像にしてください

という指摘事項が消え、パフォーマンスに関する評価が向上していることがわかります。

## (参考)WebP化実施に伴うAWSコスト計算方法
本題とは少し話がそれますが、WebP化（過去画像の一括WebP変換）時にかかるAWSコスト(S3コスト)計算方法を、最後にご紹介します。

:::note warn
大まかな費用感を把握する目的で、ざっくり計算しています。
実際にかかってくる金額とは若干相違がある点に注意してください。
（実際の金額より大きくなるように計算すると良いかと思います）
:::

### 過去画像WebP変換に伴い発生するコスト

- ストレージ保管料金（WebP化した画像のぶんだけ、保管料金がかかる）
- 画像ダウンロード料金（WebP化する元画像をS3からダウンロードする必要がある）
- 画像アップロード料金（WebP化した画像をS3にアップロードする必要がある）

の3点のコストが発生します。
それぞれについて計算していきます。

### 【事前準備】 S3特定のパスに、現在どのくらいの画像があるか調べる
↑3つのコスト算出において、S3特定パス（WebP化したい画像パス）に存在する「合計サイズ」「合計オブジェクト数」が必要になります。
調査方法はいくつかありますが、私は[aws s3api list-objects](https://docs.aws.amazon.com/cli/latest/reference/s3api/list-objects.html)を用いて、画像サイズを1つずつ調べる方法を取りました。
スクリプトは以下の感じ。

```shell
#!/bin/bash

bucket="{対象バケット名}"
prefix="{対象パス}"
total_size=0
total_object_count=0

while read -r key storage_class size; do
    # S3標準層以外も対象にしたい場合は、以下の分岐は不要
    if [ "$storage_class" = "STANDARD" ]; then
        total_size=$((total_size + size))
        total_object_count=$((total_object_count + 1))
    fi
done << END
`aws s3api list-objects --bucket "$bucket" --prefix "$prefix" --output text --query 'Contents[].[Key, StorageClass, Size]'`
END

echo "Total Size: $total_size bytes"
echo "Total Object Count: $total_object_count"
```

```
Total Size: 100426687889 bytes
Total Object Count: 193991
```

みたいな感じで結果が出力されます。

### ストレージ保管料金
- リージョン: アジアパシフィック(東京)
- ストレージクラス: S3 標準

における、オブジェクトのストレージ保持料金は

- 最初の50TB: 0.025USD/GB
- 次の450TB: 0.024USD/GB
- 500TB以上: 0.023USD/GB

です。[参考](https://aws.amazon.com/jp/s3/pricing/)

事前準備の計算より、WebP化対象画像の合計では、100426687889bytes = 93.5GB存在しています。
これらの画像をWebPに変換すると、**最大**で93.5GB 増加すると仮定します。

:::note warn
- WebP変換によりサイズが増加しない と仮定しています。
- また実際には、WebP変換で画像サイズは減るので、93.5GBより小さくなると想定されます。
:::

すると、

```
ストレージ保持料金 = 0.025 * 93.5 = 2.3375USD / 月
```

画像WebP化に伴い、月額のストレージ保持料金が多めに見て$2.3375増加見込みであることがわかりました。

### 画像ダウンロード料金
- リージョン: アジアパシフィック(東京)
- ストレージクラス: S3 標準

における、オブジェクトのダウンロード料金は

- 転送料金は無料
- GETリクエスト
    - 1000リクエストあたり0.00037USD
    - = 画像1000枚あたり0.00037USD

です。画像数(= オブジェクト数)は、193991 なので、計算式としては、

```
GETリクエストCall料金 = 193991 * 0.00037 / 1000 = 0.07177667USD
```

### 画像アップロード料金

- リージョン: アジアパシフィック(東京)
- ストレージクラス: S3 標準

における、オブジェクトのアップロード料金は

- 転送料金は無料
- PUTリクエスト
    - 1000リクエストあたり、0.0047USD
    - = 画像1000枚ごとに、0.0047USD

です。画像数(= オブジェクト数)は、193991 なので、計算式としては、

```
PUTリクエストCall料金 = 193991 * 0.0047 / 1000 = 0.9117577USD
```

### 合計のコスト感
- 合計画像サイズ: 93.5GB
- 合計画像枚数: 193991枚

くらいの規模感であれば、金額としては

- 月額増大コスト: 2.3375USD
- 過去画像一括変換時にかかるコスト(1度きり): 0.98353437USD

ほどの金額感であることがわかりました。

ただし、過去画像一括変換について、対象画像数が多い場合は処理時間が結構かかると思います。
負荷などを考慮し、別途一時サーバーを立てて処理を回す場合は、一時サーバー稼働時間分のコストもかかってきます。

# 所感
サイトで扱っている画像を全てWebPに変換するのは大変ですが、優先度の高い特定ページから順次進めていく方式だと、割と進めやすい感覚を得られました。

一方、画質を重視しているサイトとかだと、WebP化に伴い画質, 画像の見た目が損なわれていないかは、より深い検証が必要だと思います。

本記事ではsRGBの画像を例にしていますが、カメラで撮影した画像を掲載しているサイトや、画像の色味が重要な意味をもつ画像を扱っているサイトの場合は、[Adobe RGB](https://en.wikipedia.org/wiki/Adobe_RGB_color_space)の画像などでも同様の検証をするのが良いかと思います。