画像キャッシュ戦略の完全ガイド - Cache-Control, ETag, CDN の最適設定
Web 画像キャッシュの基本 - ブラウザキャッシュと CDN キャッシュの役割
画像キャッシュは Web パフォーマンスの要です。適切なキャッシュ戦略により、画像の再ダウンロードを防ぎ、ページ読み込み時間を劇的に短縮できます。キャッシュは大きく 2 層に分かれます。
ブラウザキャッシュ (クライアントサイド): ユーザーのデバイスに画像を保存し、同じ画像への再リクエストを完全に排除します。HTTP レスポンスヘッダーの Cache-Control で制御され、キャッシュヒット時はネットワークリクエストが発生しないため、表示速度は事実上ゼロ秒です。
CDN キャッシュ (エッジサーバー): オリジンサーバーの前段に配置されたエッジサーバーに画像をキャッシュします。ブラウザキャッシュがない場合でも、地理的に近いエッジサーバーから配信することでレイテンシを削減します。CloudFront、Cloudflare、Fastly などが代表的な CDN です。
キャッシュの効果: HTTP Archive の調査によると、適切なキャッシュ設定により画像リクエストの 60-80% がキャッシュから提供されます。これは帯域幅コストの削減とサーバー負荷の軽減にも直結します。特に画像は Web ページの転送量の約 50% を占めるため、キャッシュの効果が最も大きいリソースタイプです。
キャッシュ戦略の設計では、「キャッシュの鮮度 (freshness)」と「キャッシュの無効化 (invalidation)」のバランスが重要です。長すぎるキャッシュは古い画像が表示されるリスクを生み、短すぎるキャッシュはパフォーマンスの恩恵を失います。
Cache-Control ヘッダーの設計 - ディレクティブの組み合わせ方
Cache-Control は HTTP キャッシュの動作を制御する最も重要なヘッダーです。画像の種類と更新頻度に応じて、適切なディレクティブを組み合わせます。
不変の静的画像 (ハッシュ付きファイル名):
Cache-Control: public, max-age=31536000, immutable
ファイル名にコンテンツハッシュを含む画像 (例: hero-a1b2c3d4.jpg) は、内容が変わればファイル名も変わるため、1 年間 (31536000 秒) のキャッシュを安全に設定できます。immutable ディレクティブは、ブラウザがキャッシュの再検証 (conditional request) すら行わないことを示します。
更新される可能性がある画像:
Cache-Control: public, max-age=86400, must-revalidate
ユーザーアップロード画像やプロフィール画像など、同じ URL で内容が変わる可能性がある画像には、短めの max-age (1 日 = 86400 秒) と must-revalidate を設定します。キャッシュ期限切れ後は必ずサーバーに再検証を行います。
キャッシュ禁止:
Cache-Control: no-store
個人情報を含む画像 (本人確認書類など) はキャッシュしてはいけません。no-store はブラウザと中間キャッシュの両方でキャッシュを完全に禁止します。
stale-while-revalidate:
Cache-Control: public, max-age=3600, stale-while-revalidate=86400
キャッシュ期限切れ後も古いキャッシュを即座に返しつつ、バックグラウンドで再検証を行うディレクティブです。ユーザーは常に即座にレスポンスを受け取れるため、知覚速度が向上します。
ETag と条件付きリクエスト - 効率的なキャッシュ再検証
ETag (Entity Tag) は、リソースの特定バージョンを識別する文字列です。キャッシュの有効期限が切れた後、リソースが実際に変更されたかどうかを効率的に確認するために使用されます。
ETag の動作フロー:
- 初回リクエスト: サーバーがレスポンスに
ETag: "abc123"を付与 - 再リクエスト (キャッシュ期限切れ後): ブラウザが
If-None-Match: "abc123"ヘッダーを送信 - 変更なし: サーバーが
304 Not Modifiedを返す (ボディなし、数百バイト) - 変更あり: サーバーが
200 OKと新しい画像データを返す
304 Not Modified レスポンスはボディを含まないため、画像データの再転送を回避できます。1MB の画像でも、変更がなければ数百バイトの 304 レスポンスで済みます。
ETag の生成方法:
- 強い ETag: ファイルのコンテンツハッシュ (MD5、SHA-256 など) から生成。バイト単位で同一であることを保証
- 弱い ETag:
W/"abc123"形式。意味的に同等であることを示す (例: 圧縮方式が異なるが内容は同じ)
Last-Modified との比較: Last-Modified ヘッダーも条件付きリクエスト (If-Modified-Since) に使用できますが、秒単位の精度しかなく、ファイルシステムのタイムスタンプに依存するため信頼性が低い場合があります。ETag はコンテンツベースの検証であり、より正確です。CDN 環境では ETag の使用を推奨します。
注意点: 複数のオリジンサーバーがある場合、同じファイルに対して異なる ETag が生成されないよう注意が必要です。inode ベースの ETag (Apache のデフォルト) はサーバーごとに異なるため、コンテンツハッシュベースに変更してください。
CDN キャッシュの設計 - CloudFront と Cloudflare の設定例
CDN のキャッシュ設定は、オリジンサーバーの Cache-Control ヘッダーを尊重する場合と、CDN 側で独自のキャッシュポリシーを設定する場合があります。
CloudFront の設定:
- キャッシュポリシー: マネージドポリシー
CachingOptimizedは、Cache-ControlとExpiresヘッダーに基づいてキャッシュします。デフォルト TTL は 86400 秒 (1 日) - カスタムポリシー: 画像専用のキャッシュポリシーを作成し、最小 TTL を 86400、最大 TTL を 31536000 に設定。クエリ文字列をキャッシュキーに含めるかどうかも制御可能
- オリジンリクエストポリシー:
Acceptヘッダーをオリジンに転送し、WebP/AVIF の Content Negotiation を有効にする場合に設定
Cloudflare の設定:
- Browser Cache TTL: Cloudflare ダッシュボードで設定。「Respect Existing Headers」を選択すると、オリジンの
Cache-Controlをそのまま使用 - Edge Cache TTL: Page Rules で URL パターンごとに設定。
/images/*に対して Edge Cache TTL を 1 ヶ月に設定する例が一般的 - Polish: 画像の自動最適化機能。WebP 変換やロスレス圧縮を CDN エッジで実行
キャッシュキーの設計: CDN のキャッシュキーには URL パスに加えて、画像フォーマットのネゴシエーションに使用する Accept ヘッダーを含める必要があります。これにより、同じ URL でも WebP 対応ブラウザには WebP を、非対応ブラウザには JPEG をキャッシュから返せます。Vary ヘッダーに Accept を含めることで実現します。
キャッシュ無効化戦略 - 画像更新時の確実な反映方法
キャッシュの最大の課題は「古いキャッシュをいかに確実に無効化するか」です。画像を更新しても、ユーザーのブラウザや CDN に古いバージョンが残っていると、更新が反映されません。
戦略 1: コンテンツハッシュによるファイル名バージョニング
最も確実な方法です。画像ファイル名にコンテンツのハッシュ値を含めます。
hero-image-a1b2c3d4e5f6.jpg
画像が更新されるとハッシュが変わり、新しい URL になるため、古いキャッシュの問題が完全に解消されます。HTML 側の参照も同時に更新する必要があるため、ビルドパイプラインとの統合が前提です。Webpack の [contenthash] や Vite のアセットハッシュ機能で自動化できます。
戦略 2: クエリ文字列によるバージョニング
hero-image.jpg?v=20250723
ファイル名を変えずにクエリ文字列でバージョンを管理する方法です。実装が簡単ですが、一部の CDN やプロキシがクエリ文字列を無視してキャッシュする場合があるため、確実性はハッシュ方式に劣ります。
戦略 3: CDN のキャッシュパージ (Invalidation)
CDN のエッジキャッシュを強制的に削除する方法です。CloudFront では CreateInvalidation API、Cloudflare では Purge Cache API で実行できます。即座に反映されますが、エッジロケーションが多い場合は全拠点への伝播に数分かかることがあります。
推奨アプローチ: コンテンツハッシュによるファイル名バージョニングを基本とし、緊急時のみ CDN パージを併用する構成が最も堅牢です。ユーザーアップロード画像など、URL が固定される場合は短い max-age + ETag の組み合わせで対応します。
Service Worker による高度なキャッシュ制御 - オフライン対応と戦略的キャッシュ
Service Worker を使うと、ブラウザの標準キャッシュメカニズムを超えた高度なキャッシュ戦略を実装できます。画像のオフライン対応や、ネットワーク状況に応じた適応的な配信が可能になります。
Cache First 戦略 (画像に最適):
self.addEventListener("fetch", (event) => { if (event.request.destination === "image") { event.respondWith( caches.match(event.request).then((cached) => { return cached || fetch(event.request).then((response) => { const cache = await caches.open("images-v1"); cache.put(event.request, response.clone()); return response; }); }) ); }});
この戦略では、キャッシュにヒットすれば即座に返し、ミスした場合のみネットワークリクエストを行います。画像は頻繁に変更されないため、Cache First が最適です。
Stale While Revalidate 戦略: キャッシュから即座に返しつつ、バックグラウンドで最新版を取得してキャッシュを更新します。次回アクセス時には最新版が表示されます。プロフィール画像など、更新頻度が中程度の画像に適しています。
キャッシュサイズの管理: Service Worker のキャッシュストレージには容量制限があります (ブラウザにより異なるが、通常 50MB-数百 MB)。古いエントリを LRU (Least Recently Used) で自動削除するロジックを実装してください。Workbox ライブラリの ExpirationPlugin を使えば、最大エントリ数や最大保存期間を簡単に設定できます。
レスポンシブ画像のキャッシュ: srcset で複数サイズの画像を提供する場合、Service Worker でデバイスの画面幅に応じた最適なサイズのみをキャッシュすることで、ストレージ使用量を削減できます。不要な高解像度画像のキャッシュを避け、ユーザーのデバイスに適したサイズだけを保持する設計が効率的です。