コンテンツへスキップ

HTTPキャッシュヘッダーの基礎 — Cache-Control と ETag は何を制御しているのか

ブラウザの開発者ツールでネットワークタブを開くと、画像や CSS の行に Cache-ControlETag というレスポンスヘッダーが並んでいるのを見たことがある人は多いはずだ。名前は知っていても、この 2 つが実際には別々の役割を担っていることは意外と知られていない。今回は HTTP キャッシュの基本を整理した上で、自社ブログの OGP 画像生成の仕組みで実際にどちらを使い、どちらを使っていないかを見ていく。

キャッシュヘッダーが解決している問題

補足: HTTP キャッシュとは、一度取得したレスポンス(画像・CSS・JS など)をブラウザや CDN が手元に保存しておき、次回同じ URL にアクセスしたときにサーバーへ再度問い合わせずに使い回す仕組みのこと。

サーバーが何も指定しなければ、ブラウザは「このリソースを再利用してよいか」を毎回自分で判断しなければならない。多くの場合はヒューリスティック(発見的)な推測に頼ることになり、サイトによって挙動がばらつく。これを防ぐために、サーバー側がレスポンスヘッダーで明示的に指示を出す。その指示に使われる代表的なヘッダーが Cache-ControlETag であり、この 2 つは「いつまで使い回してよいか」と「まだ使い回してよいかをどう確認するか」という、別々の問いに答えている。

Cache-Control が制御していること

Cache-Control は「有効期限」を指定するヘッダーだ。自社ブログのテーマには、アイキャッチ画像が設定されていない記事向けに OGP 画像を PHP の GD ライブラリで動的生成する ogp-generator.php があり、生成した PNG を返す際に次のヘッダーを付けている。

header('Content-Type: image/png');
header('Content-Length: ' . filesize($file));
header('Cache-Control: public, max-age=2592000, immutable');

それぞれの意味は次のとおりだ。

  • public: ブラウザだけでなく、途中の CDN や共有プロキシもこのレスポンスをキャッシュしてよいという指定。個人向けのマイページのような、ユーザーごとに内容が変わるレスポンスには使えない(その場合は private にする)
  • max-age=2592000: キャッシュを「新鮮」とみなす秒数。2592000 秒はちょうど 30 日で、この間はブラウザが同じ URL に再アクセスしても、サーバーへの通信を一切発生させずに手元のキャッシュをそのまま使う
  • immutable: max-age の期間中は「絶対に内容が変わらない」という宣言。これがあると、ページを再読み込みしたときの If-None-Match を使った確認リクエスト(後述)すら省略され、ブラウザはキャッシュを無条件に使う

30 日という長い期限と immutable を組み合わせているのは、「同じ URL が指す画像は絶対に変わらない」という前提が成立しているからだ。この前提がどう作られているかは、後半で説明する。

ETag が制御していること — 「有効期限切れ後」の確認方法

Cache-Controlmax-age が切れた後、ブラウザは古いキャッシュをそのまま捨てるわけではない。「まだ使えるか」をサーバーに確認しにいく。この確認を効率化するのが ETag(Entity Tag)だ。

補足: ETag とは、そのレスポンスの中身に対して発行される短い識別子(多くはハッシュ値)のこと。中身が 1 バイトでも変われば ETag も変わる、いわば「コンテンツの指紋」。

ETag を使った確認は次のような流れになる。

  1. サーバーが最初のレスポンスで ETag: "abc123" のようなヘッダーを返す
  2. ブラウザはこの値を記憶しておき、キャッシュの有効期限が切れた後の再アクセス時に If-None-Match: "abc123" というリクエストヘッダーを付けて問い合わせる
  3. サーバー側でコンテンツを再計算し、ETag が変わっていなければ 304 Not Modified を、本文なしの軽いレスポンスとして返す
  4. ブラウザは 304 を受け取ったら、手元のキャッシュを引き続き使う

ポイントは、304 Not Modified では画像や CSS の中身そのものは転送されないという点だ。サーバー側で「中身が変わったかどうか」を判定するコストだけは発生するが、ネットワーク転送のコストは避けられる。max-age が「一定期間は確認すら省略する」という粗い制御だとすれば、ETag は「期限が切れた後も、変わっていなければ転送だけは省略する」という、より細かい制御を担っている。

なぜ ogp-generator.php は ETag を使っていないのか

ここまでの説明だけを読むと、ogp-generator.php にも ETag を実装すべきに思えるかもしれない。しかし実際のコードを見ると、そもそも ETag による確認が要らない設計になっている。

$modkey  = get_post_modified_time('YmdHis', true, $post); // GMT 基準
$cache_file = $cache_dir . "/post-{$post_id}-{$lang}-{$modkey}.png";

生成される画像ファイル名には、投稿 ID・言語・投稿の最終更新日時が埋め込まれている。つまり記事が編集されて post_modified が変わると、生成される PNG のファイル名(= URL)自体が変わる。これは「URL 自体をコンテンツのバージョンにする」という設計であり、キャッシュバスティング(cache busting)と呼ばれる手法の一種だ。

この設計の下では、「同じ URL に対して中身が変わったかを都度確認する」という ETag の出番自体が存在しない。ある URL が指す画像は、生成された瞬間から二度と変わらないことが URL の構造そのものによって保証されているからだ。だからこそ Cache-Controlimmutable を安全に付けられる。もし記事のタイトルを更新しても同じファイル名を使い回す設計だったなら、immutable は嘘の宣言になり、編集後もユーザーには古いタイトルの OGP 画像が 30 日間表示され続けるという事故につながっていただろう。

同じ「キャッシュを最新の状態に保つ」という目的に対して、ETag は「毎回サーバーに確認を取りにいく」アプローチであるのに対し、URL 自体にバージョンを埋め込む方式は「そもそも確認を発生させない」アプローチだと整理できる。後者は確認リクエストの往復すら発生しないため、ETag より制御の網が粗い代わりに、サーバー側の計算コストも通信コストも小さくて済む。どちらを選ぶかは、コンテンツがどのくらいの頻度で・どんな単位で変わるかによって決まる。

まとめ

Cache-ControlETag は、どちらも「キャッシュを制御する」という同じ目的のために存在するが、担っている役割は異なる。Cache-Controlmax-age は「いつまで無条件にキャッシュを使ってよいか」という期限を、ETag は「期限が切れた後、中身が本当に変わったかをどう安く確認するか」という検証手段を制御している。ogp-generator.php のように、URL 自体にコンテンツのバージョン(この場合は投稿の更新日時)を埋め込んでしまえば、そもそも ETag による確認プロセスを丸ごと省略でき、immutable を付けて長期間キャッシュを効かせても安全になる。どちらの手法を選ぶにせよ、前提にしている「いつ中身が変わるか」の判断を誤ると、キャッシュは効率化の道具から古い情報を配り続ける事故の原因に変わる。