HTTPキャッシュ完全ガイド——Cache-Control・ETag・CDN連携を実務で使いこなす

Cache-ControlディレクティブとETag・Last-Modifiedによる条件付きGET、CDNキャッシュ設計まで。HTTPキャッシュの仕組みを根本から理解して実務のパフォーマンス改善に活かす。

HTTPキャッシュは正しく設定すれば劇的にパフォーマンスが上がるが、誤ると古いレスポンスが返り続けるバグを生む。「とりあえずCache-Control: no-cache」で済ませているなら、もう少し深く理解する価値がある。


HTTPキャッシュの仕組み

HTTPキャッシュは、一度取得したレスポンスを保存し、同じリクエストに対して再利用する仕組みだ。

クライアント → キャッシュ → オリジンサーバ

1. キャッシュヒット: キャッシュから返す(オリジンへのリクエストなし)
2. キャッシュミス: オリジンに問い合わせてキャッシュを更新
3. 条件付きGET: キャッシュが古い場合に変更確認だけ行う

Cache-Controlヘッダ

最も重要なキャッシュ制御ヘッダ。複数のディレクティブを組み合わせて使う。

レスポンスで使う主要ディレクティブ

# 300秒間キャッシュ(プライベートキャッシュのみ)
Cache-Control: private, max-age=300

# 1時間CDN・プロキシでもキャッシュ
Cache-Control: public, max-age=3600

# キャッシュするが毎回サーバに確認(ETagと組み合わせる)
Cache-Control: no-cache

# 一切キャッシュしない
Cache-Control: no-store

# CDN等の中間キャッシュの有効期限を別途指定(s-maxage)
Cache-Control: public, max-age=60, s-maxage=86400

ディレクティブの意味を正確に理解する

ディレクティブ意味
max-age=NN秒間はキャッシュを使う
s-maxage=N共有キャッシュ(CDN・プロキシ)のmax-age
privateブラウザのみキャッシュ(CDN等は不可)
public共有キャッシュ(CDN等)も許可
no-cacheキャッシュするが毎回検証する
no-store一切保存しない
must-revalidate期限切れ後は必ずサーバに確認

よくある誤解: no-cacheは「キャッシュしない」ではなく「毎回検証する」。キャッシュを完全に無効にしたいならno-storeだ。


ETagによる条件付きGET

ETag(Entity Tag)は、リソースの内容を識別するフィンガープリントだ。これを使うと、内容が変わっていない場合に本文の再送信を省ける。

# 最初のリクエスト
GET /api/users/123 HTTP/1.1

# レスポンス(ETagを含む)
HTTP/1.1 200 OK
ETag: "abc123"
Cache-Control: no-cache
Content-Type: application/json

{ "id": 123, "name": "Taka" }
# 2回目のリクエスト(ETagを送る)
GET /api/users/123 HTTP/1.1
If-None-Match: "abc123"

# 内容が変わっていない場合
HTTP/1.1 304 Not Modified
ETag: "abc123"

# 内容が変わった場合
HTTP/1.1 200 OK
ETag: "xyz789"
Content-Type: application/json

{ "id": 123, "name": "Taka Updated" }

304 Not Modifiedが返ると、ブラウザはキャッシュのレスポンスをそのまま使う。ボディの転送がないため通信量が大幅に減る。


Last-Modified による条件付きGET

ETagの代わりに最終更新日時を使う方法。精度はETagより低い(1秒未満の変更は検知できない)が、実装が簡単だ。

# レスポンス
HTTP/1.1 200 OK
Last-Modified: Mon, 24 May 2026 12:00:00 GMT
Cache-Control: no-cache

# 次のリクエスト
GET /api/articles HTTP/1.1
If-Modified-Since: Mon, 24 May 2026 12:00:00 GMT

CDNキャッシュの設計

静的ファイルと動的APIでキャッシュ戦略を分けるのが基本だ。

静的ファイル(画像・JS・CSS)

# ファイル名にコンテンツハッシュを含める(例: main.abc123.js)
Cache-Control: public, max-age=31536000, immutable

immutableはブラウザに「このURLのリソースは変わらない」と伝え、再検証を省く。ファイル名のハッシュを変えることでキャッシュバストする設計だ。

HTML

# HTMLは短いmax-ageで、CDNには長めにキャッシュ
Cache-Control: public, max-age=0, s-maxage=3600, must-revalidate

APIレスポンス(認証あり)

# 認証が必要なAPIはprivate
Cache-Control: private, max-age=60

APIレスポンス(公開データ)

# 公開APIはCDNでもキャッシュ
Cache-Control: public, max-age=300, stale-while-revalidate=60

stale-while-revalidate=Nは「期限切れ後N秒間は古いキャッシュを返しながら、バックグラウンドで再検証する」ディレクティブ。ユーザが待たずに済む。


Next.jsでのキャッシュ設定

// app/api/products/route.ts
export async function GET() {
  const products = await fetchProducts();

  return Response.json(products, {
    headers: {
      'Cache-Control': 'public, max-age=300, stale-while-revalidate=60',
    },
  });
}
// Server ComponentでのFetch
// revalidate: 定期的に再生成
const data = await fetch('/api/data', { next: { revalidate: 300 } });

// no-store: キャッシュなし
const live = await fetch('/api/live', { cache: 'no-store' });

キャッシュ設計のチェックリスト

□ 静的アセットはファイル名にハッシュを含めているか
□ HTMLのCache-Controlは適切か(長すぎると更新が反映されない)
□ 認証が必要なAPIにprivateを設定しているか
□ 公開APIにETがを実装しているか
□ CDNのキャッシュ無効化(Invalidation)の手段があるか
□ s-maxageとmax-ageを使い分けているか

よくあるミス

1. APIレスポンスをすべてno-storeにする

セキュリティ意識から何でもno-storeにすると、CDNの恩恵がゼロになる。公開データはキャッシュしていい。

2. Cache-Controlなしで配信する

ヘッダがないと、ブラウザやCDNが独自のルールでキャッシュを判断する。明示的に設定する。

3. no-cacheとno-storeを混同する

no-cacheはキャッシュはするが毎回検証する。キャッセュさせたくないならno-storeを使う。


まとめ

シナリオ推奨設定
静的ファイル(ハッシュ付き)public, max-age=31536000, immutable
HTMLpublic, max-age=0, s-maxage=3600
公開APIpublic, max-age=300, stale-while-revalidate=60
認証済みAPIprivate, max-age=60
リアルタイムデータno-store

HTTPキャッシュの仕組みはWebを支える技術の9章(HTTPヘッダ)で体系的に解説されている。