はじめに
2021年にローンチされた Vary for images という機能を試しました。
当時試したメモがありましたが、雑すぎて、再度…
画像ファイルのオリジンが、リクエストされた画像フォーマットに応じてデータの出し分けをしているとき、CDN キャッシュでも同様に対応させる、というのが期待です。
下図は、一つの画像 URL に対し複数のフォーマットでファイルを用意したオリジンがいて、CDN を中間におきながら、リクエストに応じて最適なファイルを返している、というイメージになります。
テスト
初期状態
オリジンの設定
- 画像ファイル sample0.jpg を提供
- リクエストの Accept ヘッダを参照し、 JPEG か WebP を選んで返す
- Vary で Accept を返す
JPEG 欲しい Eyeball
JPEG をもらえます。(curl -v の出力から一部を抜粋します。以下同じ。)
> GET /sample0.jpg HTTP/1.1
> Accept: image/jpg
< Content-Type: image/jpeg
< Vary: Accept
WebP 欲しい Eyeball
WebP をもらえます。
> GET /sample0.jpg HTTP/1.1
> Accept: image/webp
< Content-Type: image/webp
< Vary: Accept
Cloudflare CDN のキャッシュを挟む
CDN デフォルト設定
- Vary for images はデフォルト
無効
- 画像ファイルのキャッシュがない状態では、最初のリクエストでキャッシュが作られる
- 後続のリクエストにはキャッシュされた画像が返される
- リクエスト中の
Accept: image/
は参照されない
JPEG 欲しい Eyeball
キャッシュのない状況で、Accept: image/jpg でリクエストします。
JPEG がもらえます。
cf-cache-status: MISS
とあるので、このリクエストで CDN にキャッシュされた可能性があります。
> GET /sample0.jpg HTTP/2
> Accept: image/jpg
< content-type: image/jpeg
< vary: Accept
< cf-cache-status: MISS
WebP 欲しい Eyeball
次に、WebP でリクエストします。
残念ながら WebP はもらえず、キャッシュされた JPEG が届きます。
cf-cache-status: HIT
となっていることから、それがわかります。
> GET /sample0.jpg HTTP/2
> Accept: image/webp
< content-type: image/jpeg
< vary: Accept
< cf-cache-status: HIT
CDN で画像フォーマットの出し分けを有効にする
JPEG と WebP の出し分けを期待し、Vary for images を有効化します。
Vary for images を有効にする設定
API: Cache Variants Setting で設定(PATCH)します。
~$ curl -s -X PATCH -H "Content-Type: application/json" -H "X-Auth-Email: $EMAIL" -H "X-Auth-Key: $APIKEY" "https://api.cloudflare.com/client/v4/zones/$ZONEID/cache/variants" -d '{"value":{
"jpeg": [
"image/webp"
],
"jpg": [
"image/webp"
]
}}'
再度リクエストを投げるまえに、キャッシュをパージします。
~$ curl -s -X POST -H "Content-Type: application/json" -H "X-Auth-Email: $EMAIL" -H "X-Auth-Key: $APIKEY" "https://api.cloudflare.com/client/v4/zones/$ZONEID/purge_cache" --data '{ "files": [
"https://$SOMEHOST/sample0.jpg"
]
}'
JPEG 欲しい Eyeball
JPEG をもらえます。
> GET /sample0.jpg HTTP/2
> Accept: image/jpg
< content-type: image/jpeg
< content-length: 4149610
< vary: Accept
< cf-cache-status: MISS
WebP 欲しい Eyeball
無事に WebP をもらえます。
> GET /sample0.jpg HTTP/2
> Accept: image/webp
< content-type: image/webp
< content-length: 1297356
< vary: Accept
< cf-cache-status: MISS
キャッシュ状態
Vary for images 無効時と違い、画像フォーマットのリクエスト内容に応じたファイルをキャッシュから取得できています。
JPEG
> GET /sample0.jpg HTTP/2
> Accept: image/jpg
< content-type: image/jpeg
< content-length: 2480841
< vary: Accept
< cf-cache-status: HIT
WebP
> GET /sample0.jpg HTTP/2
> Accept: image/webp
< content-type: image/webp
< content-length: 1297356
< vary: Accept
< cf-cache-status: HIT
結果
ユーザのリクエストした画像フォーマットに合わせデータを返すという動作をキャッシュ経由で反映させることができました。
メモ
他の画像関連機能との共存
JPEG の例を見ると cf-cache-status: MISS と HIT で、content-length が違っています。
HIT のデータサイズが小さいです(4割減)。
< content-length: 4149610
< cf-cache-status: MISS
⬇⬇⬇
< content-length: 2480841
< cf-cache-status: HIT
< cf-polished: degrade=85, origSize=4149610, status=vary_header_present
これは当該 URL に対して、画像最適化の Polish という機能が有効になっていた影響です。
下記の処理が並行して行われていました。
- 一旦キャッシュされたオリジナル画像(JPEG)から、最適化したファイル(JPEG)が作成され、キャッシュ
- 後者を以降の応答に利用
- レスポンスに cf-polished ヘッダなどが追加され、変更内容を記録
> GET /sample0.jpg HTTP/2
> Accept: image/jpg
< content-type: image/jpeg
< content-length: 4149610
< cache-control: public, max-age=691200
< last-modified: Mon, 16 Aug 2021 11:03:18 GMT
< etag: "3f516a-5c9ab2610c598"
< vary: Accept
< cf-cache-status: MISS
⬇⬇⬇
> GET /sample0.jpg HTTP/2
> Accept: image/jpg
< content-type: image/jpeg
< content-length: 2480841
< cache-control: public, max-age=691200
< cf-bgj: imgq:85,h2pri
< cf-polished: degrade=85, origSize=4149610, status=vary_header_present
< etag: "3f516a-5c9ab2610c598"
< last-modified: Mon, 16 Aug 2021 11:03:18 GMT
< vary: Accept
< cf-cache-status: HIT
問題発生 - Polish の Webp 変換
Vary for images と Polish の併用が確認できましたが、Vary for images を有効化したことで、既存サービスの一つに問題が発生しました。
Polish には JPEG(GIF, PNG)を状況に応じて WebP に変換するというオプション機能がありますが、それを活用していた既存の URL で WebP 変換およびキャッシュがされなくなりました。
- 対象のサービスのオリジンは JPEG 画像のみ配布(なので Vary も付けていない)
- Eyeball から Accept: image/webp を要求されたら、Polish で WebP に変換
- うまく動いていいたが、Vary for images を有効にしたことで、常時 Cache MISS
- .jpg に対する Accept: image/webp を Polish で扱ってたところに、同じ条件を扱う Vary for images が割り込んできた…
Polish + WebP 変換: Vary for images 無効時の挙動
Vary for images 投入前、うまく動いていたときの状況です。
一発目(JPEG 画像が得られます)
> GET /images/polish/sample0.jpg HTTP/2
> Accept: image/webp
< content-type: image/jpeg
< content-length: 80629
< cache-control: public, max-age=691200
< last-modified: Mon, 14 Aug 2023 03:27:34 GMT
< cf-cache-status: MISS
二発目(Polish で作成された WebP 画像が得られます)
> GET /images/polish/sample0.jpg HTTP/2
> Accept: image/webp
< content-type: image/webp
< content-length: 28524
< cache-control: public, max-age=691200
< cf-bgj: imgq:85,h2pri
< cf-polished: qual=85, origFmt=jpeg, origSize=80629
< last-modified: Mon, 14 Aug 2023 03:27:34 GMT
< vary: Accept
< cf-cache-status: HIT
< age: 2
Polish + WebP 変換: Vary for images 有効時の挙動
Vary for images(拡張子 jpg, jpeg に対して image/webp を定義)を有効化すると、常に Cache Miss となりました。
- CDN は sample0.jpg に対して Accept: image/webp でリクエスト
- Vary for images は content-type: image/webp と vary: accept の応答を期待
- オリジンからは content-type: image/jpeg かつ vary: accept ナシの状態で、期待外れ
- 一方、同リクエストに対しては Polish の WebP オプションも定義されている
一発目
> GET /images/polish/sample0.jpg HTTP/2
> Accept: image/webp
< content-type: image/jpeg
< content-length: 80629
< cache-control: public, max-age=691200
< last-modified: Mon, 14 Aug 2023 03:31:30 GMT
< cf-cache-status: MISS
二発目
> GET /images/polish/sample0.jpg HTTP/2
> Accept: image/webp
< content-type: image/jpeg
< content-length: 80629
< cache-control: public, max-age=691200
< last-modified: Mon, 14 Aug 2023 03:31:30 GMT
< cf-cache-status: MISS
Polish(WebP 変換を無効化): Vary for images 有効時の挙動
Polish の Webp オプションを無効にすると、キャッシュされるようになり、JPEG → JPEG での最適化は実施されます。
一発目
> GET /images/polish/sample0.jpg HTTP/2
> Accept: image/webp
< content-type: image/jpeg
< content-length: 80629
< cache-control: public, max-age=691200
< last-modified: Mon, 14 Aug 2023 03:33:07 GMT
< cf-cache-status: MISS
二発目
> GET /images/polish/sample0.jpg HTTP/2
> Accept: image/webp
< content-type: image/jpeg
< content-length: 33470
< cache-control: public, max-age=691200
< cf-bgj: imgq:85,h2pri
< cf-polished: degrade=85, origSize=80629
< last-modified: Mon, 14 Aug 2023 03:33:07 GMT
< cf-cache-status: HIT
< age: 6
解決策
今のところ Vary for images はゾーン全体での設定になります。
同じゾーンの配下で、Polish の WebP 変換オプションを活用しているサービスがある場合は、上記のように WebP 変換を外すか、オリジンでの WebP 対応を検討するなど、そこに頼らない設計としたほうが良さそうです。
あるいはオリジンはシンプルにして、CDN 側での変換に寄せていく方針もあるでしょう。Image Resizing や Images が用意されています。
補足:オリジンが Vary をつけたとき
上記は Vary for images に指定したバリアント(accept: image/webp)でオリジンにリクエストしたにも関わらず、オリジンは期待に反して content-type: image/jpeg を返し、かつ、Vary: Accept も付けない、というケースでした。
そこで、Vary for images の要件に近づけるため、オリジンで Vary: Accept を付けてみると cf-cache-status は BYPASS になりました。
これは "If the origin server sends Vary: Accept but does not serve the set variant, the response is not cached and displays BYPASS in the cache status in the response header. " の動きです。オリジンが WebP サポートしてる想定なのに、違ったからバイパスしています。
> GET /images/polish/sample0.jpg HTTP/2
> Accept: image/webp
< content-type: image/jpeg
< content-length: 80629
< vary: Accept
< cache-control: public, max-age=691200
< cf-cache-status: BYPASS
Polish と Vary
一方、Polish と Vary ヘッダーについては下記の注意書きがあります。
"Polish may not be applied to origin responses that contain a Vary header. The only accepted Vary header is Vary: Accept-Encoding."
Vary for images との併用がどこまで想定されているかは未確認です。
パージ
キャッシュされたファイルがパージされると、バリアントも同様にパージされます。
ブラウザからのアクセス
ブラウザからもアクセスしてログを確認します。ダッシュボードの Instant Logs から抜粋します。
iOS Safari(Accept: */*;q=0.8)
--> JPEG でキャッシュされています
"ClientDeviceType": "mobile",
"ClientRequestUserAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1",
"RequestHeaders": {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
},
"ResponseHeaders": {
"content-type": "image/jpeg",
"vary": "Accept, Accept-Encoding"
},
macOS Chrome(Accept: image/avif,image/webp,image/apng,*/*;q=0.8)
--> WebP でキャッシュされています
"ClientDeviceType": "desktop",
"ClientRequestUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36",
"RequestHeaders": {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
},
"ResponseHeaders": {
"content-type": "image/webp",
"vary": "Accept, Accept-Encoding"
},
【便利】 ログのカスタムフィールド
見たいヘッダを追加しておけば Instant Logs にもサクッと反映されます。
その他、参考情報
API
- Vary for images を無効にする(DELETE)
~ $ curl -s -X DELETE -H "Content-Type: application/json" -H "X-Auth-Email: $EMAIL" -H "X-Auth-Key: $APIKEY" "https://api.cloudflare.com/client/v4/zones/$ZONEID/cache/variants"
{
"result": {
"editable": true,
"id": "variants",
"modified_on": "2023-08-03T11:28:31.818839Z"
},
"success": true,
"errors": [],
"messages": []
}
- Vary for images の現状を得る(GET)
無効時の例
~ $ curl -s -X GET -H "Content-Type: application/json" -H "X-Auth-Email: $EMAIL" -H "X-Auth-Key: $APIKEY" "https://api.cloudflare.com/client/v4/zones/$ZONEID/cache/variants"
{
"result": null,
"success": false,
"errors": [
{
"code": 1142,
"message": "Unable to retrieve variants setting value. The zone setting does not exist."
}
],
"messages": []
}
リンク
https://blog.cloudflare.com/vary-for-images-serve-the-correct-images-to-the-correct-browsers/
https://developers.cloudflare.com/api/operations/zone-cache-settings-change-variants-setting
https://developers.cloudflare.com/cache/advanced-configuration/vary-for-images/
https://developers.cloudflare.com/images/polish/compression/
https://developers.cloudflare.com/images/polish/cf-polished-statuses/
https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation/List_of_default_Accept_values#values_for_an_image