LightFile Proxyでは、URLの拡張子を.jpgや.pngとしたまま、データの中身を軽量な次世代画像フォーマットのWebPに変換して配信する。このアプローチによって、従来のHTMLに変更を加えず、画像だけを最適化できるのだが、人によっては「気持ち悪い」と感じるかもしれない。
しかしこのアプローチは画像最適化で広く使われていて、CDN各社も同じ考え方の機能を持っている。とはいえ「よそもやっているから」では答えにならない。このアプローチが妥当であることを、一次情報に当たりながら解説したい。
連載「詳説 LightFile Proxy」
AWS CloudFrontの後ろに置いて、既存のサイトを変えずに画像をWebPで配信する仕組みを説明する連載。
- サイトに手を加えず画像をWebPで配信する仕組み
- 拡張子は.jpgのまま中身をWebPにしてよいのか(本記事)
- 画像変換のタイミングとその最適解(9月27日公開予定)
- 次世代画像フォーマットの採用でどのくらい画像が軽くなるか(9月28日公開予定)
- WebP化によるデータ削減で見た目は劣化するか(9月29日公開予定)
- 画像のリンク切れを起こさない二重の障害対応(9月30日公開予定)
- 無駄な変換を避け高速に画像を配信するためのキャッシュ戦略(10月1日公開予定)
- その他の細かな特徴(10月2日公開予定)
- AWS純正のDITで代替できるか(10月3日公開予定)
- 料金体系(10月4日公開予定)
ブラウザの振る舞いを決めているWHATWGの仕様
現在の主要なブラウザは、WHATWG(Web Hypertext Application Technology Working Group)が定める仕様に沿って動いている。2004年にApple・Mozilla・Operaが立ち上げ、2017年からはApple・Google・Microsoft・Mozillaの4社が運営している団体で、HTMLやDOM、fetchといった中心的な仕様を「Living Standard」として維持している。ブラウザを作っている当事者が、自分たちの実装の仕様を書いている格好だ。
その中に、今回の疑問にそのまま答える仕様がある。MIME Sniffing Standardで、サーバーから届いたデータの種類をブラウザがどう判断するかを定めたものだ。
拡張子は何を決めているのか
手元のパソコンでは拡張子がファイルの種類を決めている。.jpg を .txt に変えればテキストエディタが開こうとする。この感覚があるので、拡張子と中身の食い違いが気になる。
HTTPで取得したリソースでは事情が違う。MIME Sniffing Standardの「5.1. Interpreting the resource metadata」にこう書かれている。
File extensions are not used to determine the supplied MIME type of a resource retrieved via HTTP because they are unreliable and easily spoofed.
(HTTPで取得したリソースの供給MIMEタイプを決めるのに拡張子は使わない。信頼できず、簡単に偽装できるからである)
ブラウザが型を決める材料はURLの末尾ではなくContent-Typeヘッダだ。URLはそのリソースの置き場所を指しているだけで、中身の種類を約束してはいない。
画像はバイト列で判定される
画像についてはもう一段ある。同じ仕様の「8.2. Sniffing in an image context」では、画像として取得したリソースの型をこう決めると書かれている。
- 供給された型がXML系ならそれを使う
- そうでなければ「6.1. Matching an image type pattern」で、データの先頭のバイト列を照合する
- 照合できたら、その結果を型として採用する
- 照合できなかったときだけ、宣言された型を使う
極端な話、拡張子やMIMEタイプが何であれ、実際のバイト列がWebPならWebP画像として表示される。WebPのバイト列は先頭が RIFF で始まり、少し後ろに WEBP が続く。この形は仕様の照合表に載っていて、JPEGの FF D8 FF やPNGの 89 50 4E 47 と同じ扱いになっている。
なおX-Content-Type-Options: nosniffを付けると、ブラウザはこの照合をやめて宣言された型をそのまま信じる。
LightFile Proxyはimage/webpを付けて返す
LightFile ProxyはWebPに変換した画像を返すとき、Content-Type: image/webp を付けて返す。宣言と中身は一致していて、従来のままなのはURLの見た目だけだ。

したがってバイト列の照合に頼ってもいないし、nosniffと衝突することもない。仕様のどの経路を通ってもWebPとして解釈される。食い違っているのは拡張子だけで、拡張子はもともと型判定に使われていない。
受け取れる形式を伝えるAcceptヘッダ
ここで、WebP非対応のブラウザに何が届くのかも確かめておきたい。その前に、振り分けの材料になるAcceptヘッダへ触れておく。
HTTPにはコンテントネゴシエーションという仕組みがある。クライアントが「自分はこの形式を受け取れる」とリクエストのAcceptヘッダで伝え、サーバーはその中から選んで、選んだ結果をContent-Typeで返す。
ブラウザは画像を取りに行くときにも画像用のAcceptを送っている。MDNがブラウザごとの既定値を一覧にしているので、そこから引くと現在の値はこうなっている。
Chrome / Edge 121以降
image/avif,image/webp,image/apng,image/*,*/*;q=0.8
Firefox 128以降
image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5
Safari(macOS Big Sur以降)
image/webp,image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5どれにもimage/webpが入っている。WebPに対応したブラウザは、慣例としてAcceptにimage/webpを並べる。Firefoxは65から、SafariはmacOS Big Sur(Safari 14)からこの形になった。それ以前のFirefoxは*/*だけを送っていて、Big Sur以前のSafariの一覧にもimage/webpは無い。
仕様で義務づけられた動作ではないが、主要ブラウザがこれに揃っているので、image/webpが並んでいるかどうかを対応可否の目印として使える。WebPに対応していない古いブラウザや、WebPを知らないツールは並べてこない。
WebP非対応のブラウザにはオリジナルを返す
LightFile ProxyはこのAcceptにimage/webpが含まれているかどうかだけを見る。含まれていれば変換済みを、含まれていなければオリジナルを返す。

判定は保守的にしてある。Accept: */* のように「何でも受け取る」と書かれているだけの相手にはWebPを送らない。WebPを知らないツールやクローラーが*/*を送ってくるためで、対応していると明示した相手にだけ変換済みを渡している。
判定に使ったヘッダはVaryに入れて返す。CloudFrontから見ると、同じURLでもAcceptの内容が違えば別のキャッシュになるので、対応ブラウザに届いたWebPが非対応ブラウザへ配信されることはない。CloudFront側で判定させて専用のヘッダで渡す構成もあり、その場合はVaryもそのヘッダになる。
振り分けを確かめたいときは、以前に公開したWebPやAVIFに対応していない旧ブラウザのふりをするChrome拡張機能が使える。Acceptを書き換えて非対応ブラウザを装う拡張で、実機を用意せずに両方の経路を踏める。
オリジナルは書き換えない
WebPになるのはCloudFrontを通ってエンドユーザーへ配信されるデータだけで、オリジンサーバーやS3バケットに置かれている従来フォーマットのJPEG/PNG/GIFは書き換わらない。変換した結果は変換した側が持っていて、オリジンには書き戻さない。
画像のオリジナルは今までどおりの形式で管理でき、CMSの管理画面から見える画像も、デザイナーが書き出す画像ファイルも今までどおりになる。LightFile Proxyをやめれば、その日から従来フォーマットの配信に戻る。
副作用
問題はないと書いたが、知らないと戸惑う挙動はある。
- 保存したファイルの名前は
.jpgのまま。 対応ブラウザで画像を右クリックして保存すると、item.jpgという名前でWebPのデータが保存される。ブラウザや画像ビューアは中身で判断するので開けるが、拡張子で形式を判断するツールに渡すと扱えないことがある - ダウンロード用のリンクには向かない。 印刷用データのように配布を目的としたファイルは、変換の対象から外す設定にしておきたい
どちらも、配信のときだけ中身を差し替えているために起きる。オリジナルはそのままなので、元の形式が必要ならオリジンから取ればよい。
もうひとつよく聞かれる「変換する分だけ表示が遅くならないか」には、連載の別の記事で答えている。
- (3) 画像変換のタイミングとその最適解