コンテントネゴシエーションで最適な画像を配信 - Accept ヘッダーと CDN の連携
コンテントネゴシエーションとは - HTTP の仕組みで最適フォーマットを選択
コンテントネゴシエーション (Content Negotiation) は、HTTP プロトコルの標準機能を使い、クライアントとサーバーが最適なレスポンス形式を交渉する仕組みです。画像配信においては、ブラウザが対応する画像フォーマットをサーバーに伝え、サーバーが最も効率的なフォーマットで応答します。
動作の流れ:
- ブラウザがリクエストヘッダーに
Accept: image/avif,image/webp,image/png,image/*を送信 - サーバーが Accept ヘッダーを解析し、優先度の高いフォーマットから順に対応可能か確認
- AVIF に対応していれば AVIF を、WebP のみ対応なら WebP を、どちらも非対応なら JPEG/PNG を返す
- レスポンスに
Vary: Acceptヘッダーを付与し、キャッシュの正確性を保証
利点: HTML の <picture> 要素による分岐と異なり、URL を変更せずにフォーマットを切り替えられます。既存の <img src="photo.jpg"> をそのまま維持しながら、対応ブラウザには WebP や AVIF を配信できるため、HTML の変更が不要です。
ブラウザの Accept ヘッダー例:
- Chrome 100+:
image/avif,image/webp,image/apng,image/*,*/*;q=0.8 - Firefox 93+:
image/avif,image/webp,*/* - Safari 16+:
image/webp,image/png,image/*;q=0.8,*/*;q=0.5
Safari は 2023 年の iOS 16 / macOS Ventura から WebP に対応し、AVIF は Safari 16.4 以降で対応しています。
サーバーサイドでの実装 - Nginx と Apache の設定例
Web サーバーでコンテントネゴシエーションを実装する方法を、Nginx と Apache それぞれの設定例で解説します。
Nginx の設定:
map $http_accept $img_suffix { default ""; "~image/avif" ".avif"; "~image/webp" ".webp";}server { location ~* ^(.+)\.(jpe?g|png)$ { set $base $1; set $ext $2; add_header Vary Accept; try_files $base$img_suffix.$ext $uri =404; }}
この設定では、Accept ヘッダーに image/avif が含まれていれば .avif 版を、image/webp が含まれていれば .webp 版を優先的に返します。該当ファイルが存在しない場合は元の JPEG/PNG にフォールバックします。
Apache の設定 (.htaccess):
RewriteEngine OnRewriteCond %{HTTP_ACCEPT} image/avifRewriteCond %{REQUEST_FILENAME}.avif -fRewriteRule ^(.+)\.(jpe?g|png)$ $1.$2.avif [T=image/avif,L]RewriteCond %{HTTP_ACCEPT} image/webpRewriteCond %{REQUEST_FILENAME}.webp -fRewriteRule ^(.+)\.(jpe?g|png)$ $1.$2.webp [T=image/webp,L]Header append Vary Accept
重要な注意点: Vary: Accept ヘッダーを必ず付与してください。これがないと、CDN やプロキシが AVIF 版をキャッシュし、WebP しか対応しないブラウザにも AVIF を返してしまう事故が発生します。
CDN でのコンテントネゴシエーション - CloudFront と Cloudflare の設定
CDN を使用する場合、コンテントネゴシエーションの設定は CDN 側で行うのが効率的です。エッジサーバーでフォーマット判定を行うことで、オリジンへのリクエストを最小化できます。
CloudFront の設定:
- キャッシュポリシー:
Acceptヘッダーをキャッシュキーに含めるカスタムポリシーを作成。ただし Accept ヘッダーの値はブラウザごとに微妙に異なるため、キャッシュヒット率が低下する問題がある - Lambda@Edge: Viewer Request トリガーで Accept ヘッダーを正規化し、
image/avif、image/webp、otherの 3 パターンに集約。これによりキャッシュヒット率を大幅に改善 - CloudFront Functions: Lambda@Edge より軽量で高速。Accept ヘッダーの解析とリクエスト URI の書き換えに最適
CloudFront Functions の実装例:
function handler(event) { var request = event.request; var accept = request.headers.accept ? request.headers.accept.value : ''; var uri = request.uri; if (uri.match(/\.(jpe?g|png)$/i)) { if (accept.includes('image/avif')) { request.uri = uri + '.avif'; } else if (accept.includes('image/webp')) { request.uri = uri + '.webp'; } } return request;}
Cloudflare の設定: Cloudflare の Polish 機能を有効にすると、WebP への自動変換がエッジで行われます。Pro プラン以上では AVIF 変換も利用可能です。Transform Rules でカスタムロジックを実装することも可能です。
Vary ヘッダーの正しい運用 - キャッシュ事故を防ぐ
Vary ヘッダーはコンテントネゴシエーションの正確なキャッシュ動作を保証する重要な要素です。設定を誤ると、間違ったフォーマットがキャッシュから配信される深刻な事故が発生します。
Vary ヘッダーの役割: Vary: Accept は「このレスポンスは Accept ヘッダーの値によって異なる内容を返す」ことをキャッシュに伝えます。CDN やプロキシはこの情報を元に、Accept ヘッダーの値ごとに別々のキャッシュエントリを保持します。
よくある事故パターン:
- Vary なし: CDN が最初のリクエスト (Chrome) の AVIF レスポンスをキャッシュし、次のリクエスト (Safari) にも AVIF を返す → Safari で画像が表示されない
- Vary: *: すべてのリクエストヘッダーが異なればキャッシュしない設定。事実上キャッシュ無効化と同じで、CDN の意味がなくなる
- Vary: Accept-Encoding, Accept: 正しいが、キャッシュのバリエーションが増えすぎてヒット率が低下する場合がある
Accept ヘッダーの正規化: ブラウザごとに Accept ヘッダーの値が微妙に異なるため (品質値の有無、順序の違いなど)、そのままキャッシュキーにすると同じフォーマットに対応するブラウザでも別キャッシュになります。CDN のエッジで Accept ヘッダーを avif、webp、default の 3 値に正規化することで、キャッシュヒット率を最大化できます。
テスト方法: curl -H "Accept: image/webp" -I https://example.com/photo.jpg でレスポンスヘッダーを確認し、Content-Type: image/webp と Vary: Accept が返ることを検証します。異なる Accept ヘッダーで複数回リクエストし、正しいフォーマットが返ることを確認してください。
picture 要素との比較と使い分け - クライアントサイド vs サーバーサイド
画像フォーマットの出し分けには、HTML の <picture> 要素によるクライアントサイド方式と、コンテントネゴシエーションによるサーバーサイド方式の 2 つがあります。それぞれの特徴を理解し、適切に使い分けることが重要です。
picture 要素 (クライアントサイド):
<picture> <source srcset="photo.avif" type="image/avif"> <source srcset="photo.webp" type="image/webp"> <img src="photo.jpg" alt="写真"></picture>
比較表:
- HTML 変更: picture 要素は全ページの HTML 修正が必要。コンテントネゴシエーションは HTML 変更不要
- URL 構造: picture 要素はフォーマットごとに異なる URL。コンテントネゴシエーションは同一 URL
- キャッシュ効率: picture 要素はフォーマットごとに独立キャッシュ。コンテントネゴシエーションは Vary ヘッダーで管理
- SEO: picture 要素は各 URL が独立してインデックス可能。コンテントネゴシエーションは 1 URL で管理
- デバッグ: picture 要素はブラウザの DevTools で確認容易。コンテントネゴシエーションはヘッダー確認が必要
推奨する使い分け:
- 新規プロジェクト:
<picture>要素を推奨。明示的で予測可能な動作 - 既存プロジェクトの改善: コンテントネゴシエーションを推奨。HTML 変更なしで導入可能
- CMS / UGC: コンテントネゴシエーションを推奨。ユーザーがアップロードした画像の HTML を変更できない
トラブルシューティングと監視 - 本番環境での運用
コンテントネゴシエーションは正しく設定すれば透過的に動作しますが、設定ミスや CDN の挙動変更により問題が発生することがあります。本番環境での監視とトラブルシューティング手法を紹介します。
よくある問題と対処法:
- 画像が表示されない: Accept ヘッダーの解析ロジックにバグがあり、非対応フォーマットを返している。curl で各ブラウザの Accept ヘッダーをシミュレートして検証
- キャッシュヒット率が低い: Accept ヘッダーの正規化が不十分。CDN のキャッシュ統計で Vary ごとのヒット率を確認
- AVIF が配信されない: ファイルが存在しない、または MIME タイプの設定漏れ。
image/avifが Web サーバーの MIME タイプに登録されているか確認 - ファイルサイズが増加: 稀に WebP/AVIF が元の JPEG より大きくなるケースがある。変換時にサイズ比較を行い、小さい方を採用するロジックを追加
監視の実装:
- CDN のアクセスログから
Content-Typeの分布を集計し、AVIF/WebP の配信比率を追跡 - Real User Monitoring (RUM) で画像の読み込み時間をフォーマット別に計測
- 定期的に主要ブラウザで画像が正しく表示されることを自動テスト (Playwright / Puppeteer)
フォールバックの確実性: コンテントネゴシエーションの最も重要な要件は、対応フォーマットがない場合に確実に元画像 (JPEG/PNG) にフォールバックすることです。新しいフォーマットの追加時は、フォールバックパスが壊れていないことを必ずテストしてください。エッジケースとして、Accept ヘッダーが空のリクエスト (一部のボットやプロキシ) にも正しく応答できることを確認します。