こんにちは、普段PR TIMES STORY(以下STORY)の開発リーダーをしている岩下(@iwashi623)です。
今回はSTORYのサムネイル画像の配信フォーマットを変更することによるパフォーマンス改善を行ったので、そちらについて書いていこうと思います。
現状の問題点
STORYでは配信のサムネイルにユーザーからアップロードした画像を3サイズ(width800, 400, 200)にリサイズした画像を使用していました。

リサイズした画像は表示する箇所ごとに適したサイズのサムネイルを選択して、画像を配信していました。
そのような状況のSTORYで、Lighthouseを使ってパフォーマンスを計測した結果が以下です。一枚目がmobile(スマホ)、二枚目がDesktop(PC)の結果となります。


パフォーマンスの数値が悪いことがわかります。
また、パフォーマンスの原因が悪い詳細な理由(ボトルネック)については以下のようになっています。

特に「Serve images in next-gen formats」の項目がパフォーマンスを悪化させている原因だということがわかります。
こちらの項目には、
Image formats like WebP and AVIF often provide better compression than PNG or JPEG, which means faster downloads and less data consumption.
(訳: WebPやAVIFなどの画像フォーマットは、PNGやJPEGよりも圧縮率が高いことが多いため、ダウンロードの高速化とデータ消費量の削減が可能です)
と書いてあり、配信しているサムネイル画像のフォーマットがよくないことがわかります。
以前PR TIMESはFastlyのImage Optimizerを使用して、サムネイル画像にWebPを使用することでプレスリリースのサムネイル画像の高画質化と軽量化を達成しました。

そのため、STORYも同様の仕組みを使ってサムネイル画像の軽量化を達成しようと考えました。
改善方法
まず、リサイズした複数種類のサムネイル画像生成をやめて、前述したブログで述べられているように大きいサイズのサムネイル画像を一枚だけS3にアップロードするようにしました。

Fastly Image Optimizerの設定は基本的にはPR TIMESと同様の設定を行いましたが、一部差異があるため、今回は差異の部分について述べていきます。
AVIFとWebPの両方を使用する
PR TIMESでは、サムネイル画像の配信にWebPを採用しましたが、STORYではAVIFを基本的に使用しています。
AVIFもまた、WebPと同様に新しい静止画のフォーマットです。JPEGやPNGと比較して低いqualityにしても画像の劣化が少なく、結果として小さいファイルサイズで画像を配信できます。
AVIFとWebPの違いは
- WebPはGoogleが開発した画像フォーマット
- モダンブラウザはほとんどが対応している(古いiOSなどでは一部非対応)
- JPEGと比較して高い圧縮率を誇る
- AVIFはAmazon・Netflix・Google・Microsoft・Mozilla等の幅広い企業が共同で開発したWebPよりも新しい画像フォーマット
- Chrome, Firefox, Safariなど続々とサポートするブラウザが増えている。
- しかしながらWebPと比較すると対応ブラウザはまだ少ない
- WebPよりも性能が良いという報告がある
のようになっています。
FastlyのImage OptimizerはAVIFへの変換をサポートしているため、今回AVIFでのサムネイル画像配信を採用しました。具体的には、
- AVIF対応ブラウザにはAVIFを返す
- AVIF非対応かつWebP対応ブラウザにはWebPを返す
- AVIF・WebPともに非対応ブラウザにはJPEGを返す
のような設定を実装しました。
Image Optimizerが有効になっている時、autoパラメーターを使って画像へのリクエストにauto=webp
やauto=avif
のようなクエリパラメーターを付与すると、自動でWebPやAVIFへの変換を行ってくれます。
しかしながら、AVIF・WebPのどちらか一方ではなく、今回のケースのようにAVIFとWebPの両方を使い分けたいことがあると思います。その場合、ブラウザのacceptヘッダーによって処理を分岐するために、vcl_recvに
if (querystring.get(req.url, "auto") == "avif,webp") {
if (req.http.Accept ~ "\bimage/avif\b") {
set req.url = querystring.set(req.url, "auto", "avif");
} elsif (req.http.Accept ~ "\bimage/webp\b") {
set req.url = querystring.set(req.url, "auto", "webp");
}
}
のように記述する必要があるので注意が必要です。
上記のような設定をすると、auto=avif,webp
のようなクエリパラメーターを加えて画像のリクエストを送信した時、acceptヘッダーに基づいて条件に合致する画像をレスポンスすることができます。
また、今回はJPEG・WebP・AVIFで画像のqualityもそれぞれ変更できるようにしたいと思いました。画像フォーマットごとに指定できる仕組みがあると、リリース後にフォーマットごとにqualityの値を調整できるためより便利だと考えたからです。
Image Optimizerのqualityパラメーターは、デフォルトでquality=85,75
のように第2引数を渡すことができて、auto=webp
パラメーターがある際に、WebPとそれ以外の画像のqualityを指定することができます。しかしながら、auto=avif,webp
がリクエストに含まれていたときに、3つの引数を渡してAVIFのみqualityを指定するような設定は現在サポートされていません。そのためVCLを
sub vcl_recv {
declare local var.parsedRequestQualityParam STRING;
declare local var.quality STRING;
set var.parsedRequestQualityParam = regsuball(querystring.get(req.url, "quality"), "%252C", ",");
if (querystring.get(req.url, "auto") == "avif,webp") {
if (req.http.Accept ~ "\bimage/avif\b") {
set req.url = querystring.set(req.url, "auto", "avif");
set var.quality = regsuball(var.parsedRequestQualityParam, "([0-9]+),([0-9]+),([0-9]+)", "\3");
set req.url = querystring.set(req.url, "quality", var.quality);
return(lookup);
} elsif (req.http.Accept ~ "\bimage/webp\b") {
set req.url = querystring.set(req.url, "auto", "webp");
}
}
set var.quality = regsuball(var.parsedRequestQualityParam, "([0-9]+),([0-9]+),([0-9]+)", "\1\,\2");
set req.url = querystring.set(req.url, "quality", var.quality);
return(lookup);
}
のようにカスタムすることで実現しました。
上記のようなカスタムVCLを使用することで、format=jpeg&auto=avif,webp&quality=85,75,70
のようなクエリパラメーターがついたリクエストが来た際、
- AVIF対応ブラウザには画像をquality70のAVIFに変換して配信
- AVIF非対応かつWebP対応ブラウザには画像をquality75のWebPに変換して配信
- それ以外のブラウザには画像をquality85のJPEGに変換して配信
することができます。
AVIFとWebPにqualityの差をつけた理由は、検証段階で同qualityで比較した結果、AVIFの方が高画質であったものの、ファイルの容量が大きくなってしまったためです。AVIFとWebPがおよそ同じようなファイルサイズになるように調整した結果、WebPは75, AVIFは70という数値に落ち着きました。
Surrogate-Keyを用いたCache Purgeの導入
新規でアップロードされる画像に関してはS3にアップロードする際にx-amz-meta-surrogate-key
ヘッダーを付与することにしました。これは、Fastlyで画像のキャッシュを消す際、任意のSurrogate-Keyを指定して特定のキャッシュを消す際に利用できるためです。
STORYはLaravel + AWS SDKを用いて画像をアップロードしています。なので、
's3_options' => [
'CacheControl' => 'public, max-age=31536000',
'Metadata' => [
'surrogate-key' => 'hoge piyo bar fuga',
]
]
のように、アップロード時のMetadataとしてsurrogate-keyオプションを渡してあげるとx-amz-meta-surrogate-key
ヘッダーがアップロードされたオブジェクトに付与できます。
Fastly側はDocumentを参考にして、Surrogate-Keyの設定を行いました。
Surrogate-Keyはアップロード時にスペース区切りで複数付与できるので、
x-amz-meta-surrogate-key="hoge piyo bar fuga"
のように設定できます。この画像のキャッシュはhoge, piyo, bar, fugaのいずれかのKeyを指定したキャッシュパージをAPIまたはFastlyコンソール上から実行することで削除することができます。
改善の効果
まず、パフォーマンス面ですが大幅な向上を達成することができました。
スマートフォン表示のLighthouseの結果は

と、パフォーマンスの項目は25→66と大幅に向上しました。
PCでの表示についても、

パフォーマンスの項目が64→99と大きく向上している事がわかります。
パフォーマンスのボトルネックについても、「Serve images in next-gen formats」から別の箇所に変わりました。

またサムネイル画像の容量は、画像によりますが観測した範囲で
改善前 → content-length: 642954
改善後 → content-length: 23141
のような大きな差が生まれている画像がありました。
さらに小さいサムネイル画像については、今までの個別でリサイズしていた画像よりも大きな横幅の画像を返すようにしたこともあり、画像の容量が減少しているにも関わらず見た目が以下のように改善しました。

さいごに
ファイルサイズを小さくしてパフォーマンスを向上させたり、画像の見た目をきれいにしたりすることはユーザー体験に直結するため今回の変更を加えてよかったなと改めて実感します。
FastlyはVCLをカスタムして各々の柔軟な設定ができたり、設定の変更やキャッシュパージの時間が早かったりと使ってみてとても楽しい技術でした。
今回の改修で使用したFastlyや、その他の様々なサービスを活用したアプリケーションのパフォーマンス改善に興味のある方は、ぜひカジュアル面談でお話できると嬉しいです。