Cache-Controlとは
Cache-Controlは、HTTPレスポンスやリクエストに付与され、キャッシュの保存・再利用・検証ルールを細かく制御するヘッダーです。適切に設定することで、サーバー負荷を削減し、ページ表示速度を大幅に改善できます。
なぜ必要か: 同じリソースを毎回サーバーから取得すると、通信コストと待ち時間が無駄になります。キャッシュを活用することで、ユーザー体験が向上し、インフラコストも削減できます。
キャッシュの基本フロー
1. 初回リクエスト
ブラウザがリソースを初めて取得します。
GET /style.css HTTP/1.1
Host: example.com
レスポンス:
HTTP/1.1 200 OK
Content-Type: text/css
Cache-Control: public, max-age=31536000
Content-Length: 1234
2. キャッシュからの取得
有効期限内なら、ブラウザはサーバーに問い合わせず、キャッシュから直接取得します(ネットワークタブに「(disk cache)」と表示)。
3. 期限切れ後の再検証
max-age期限が切れると、ブラウザはサーバーに検証リクエストを送ります。
GET /style.css HTTP/1.1
If-None-Match: "abc123"
内容が変わっていない場合:
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=31536000
ポイント: 304 Not Modifiedレスポンスはボディを含まないため、転送量が大幅に削減されます。ETagやLast-Modifiedと組み合わせて使います。
Cache-Controlディレクティブ一覧

キャッシュ可否の制御
| ディレクティブ | 説明 | 用途 |
|---|---|---|
| public | 共有キャッシュにも保存できることを明示 | 認証付きでも共有してよい公開リソース |
| private | 共有キャッシュへの保存を禁止 | ユーザー固有のデータ |
| no-cache | 保存はできるが、再利用前に検証が必要 | 常に最新か確認したいリソース |
| no-store | このレスポンスをキャッシュへ保存しない | 機密性の高いレスポンス |
有効期限の制御
| ディレクティブ | 説明 | 例 |
|---|---|---|
| max-age=N | ブラウザ用の有効期限(秒) | max-age=3600 (1時間) |
| s-maxage=N | CDN/共有キャッシュ用の有効期限 | s-maxage=86400 (24時間) |
| immutable | freshな期間は更新確認が不要だと示す | max-age=31536000, immutable |
再検証の制御
| ディレクティブ | 説明 | 用途 |
|---|---|---|
| must-revalidate | staleになった後、通常は検証なしで再利用しない | 鮮度を厳密に扱うデータ |
| proxy-revalidate | CDN向けのmust-revalidate | CDNだけ厳密にしたい場合 |
| stale-while-revalidate=N | 期限切れ後N秒は古いキャッシュを返しつつ裏で更新 | 高速応答と鮮度の両立 |
| stale-if-error=N | エラー時は期限切れキャッシュをN秒まで利用 | 可用性重視 |
実践メモ: immutableは、内容が変わればURLも変わるハッシュ付きファイル(例: `app.a1b2c3.js`)に向いています。freshな期間の不要な再検証を減らせます。
よく使うパターン
静的アセット(JS/CSS/画像)
ハッシュ付きファイル名の場合:
Cache-Control: public, max-age=31536000, immutable
- 理由: ファイル名が変わらない限り内容も不変。1年間キャッシュOK。
- 例:
app.a1b2c3.js,logo.7f8e9d.png
ハッシュなしファイル名の場合:
Cache-Control: public, max-age=3600
- 理由: 1時間程度のキャッシュで、更新にも対応。
- 例:
style.css,main.js
HTML
Cache-Control: public, max-age=0, must-revalidate
または
Cache-Control: no-cache
- 理由: HTMLは常に最新を確認。ただし304で高速応答。
- 例:
index.html,about.html
API レスポンス
公開データ:
Cache-Control: public, max-age=60, s-maxage=300
- ブラウザ: 1分キャッシュ
- CDN: 5分キャッシュ
ユーザー固有データ:
Cache-Control: private, max-age=0, no-cache
- 理由: CDNにキャッシュさせず、ブラウザも毎回検証。
機密性の高いレスポンス:
Cache-Control: no-store
- 理由: このレスポンスをHTTPキャッシュへ保存させない。
CDN配信(静的サイト)
Cache-Control: public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800
- ブラウザ: 1時間
- CDN: 24時間
- 古いキャッシュも1週間は裏で更新しつつ即応答
注意: no-cacheは「キャッシュしない」ではなく「毎回検証する」という意味です。「キャッシュしない」はno-storeを使います。
no-cache vs no-store の違い
no-cache
Cache-Control: no-cache
動作:
- キャッシュに保存する
- 使用前に必ずサーバーに検証
- 304 Not Modifiedなら再利用
用途: 常に最新を確認したいが、変更がなければキャッシュを活用したい場合。
no-store
Cache-Control: no-store
動作:
- このレスポンスをキャッシュに保存しない
- 毎回サーバーから全データを取得
- 304レスポンスも使わない
用途: 機密情報、個人情報、銀行の残高など。
no-storeは、すでに保存済みのレスポンスを削除する命令ではなく、ブラウザ履歴などHTTPキャッシュ以外の保存まで消去する保証もありません。漏えい対策は、認可・ログ・画面表示なども含めて設計します。
実装例
Express.js
const express = require('express');
const app = express();
// 静的ファイル(ハッシュ付き)
app.use('/assets', express.static('public/assets', {
maxAge: '1y',
immutable: true
}));
// 静的ファイル(ハッシュなし)
app.use('/images', express.static('public/images', {
maxAge: '1h'
}));
// HTML
app.get('/', (req, res) => {
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
res.sendFile('index.html');
});
// API(公開データ)
app.get('/api/posts', (req, res) => {
res.set('Cache-Control', 'public, max-age=60, s-maxage=300');
res.json({ posts: [...] });
});
// API(ユーザー固有)
app.get('/api/me', (req, res) => {
res.set('Cache-Control', 'private, no-cache');
res.json({ user: {...} });
});
// 機密情報
app.get('/api/payment', (req, res) => {
res.set('Cache-Control', 'no-store');
res.json({ card: {...} });
});
Nginx
# ハッシュ付き静的ファイル
location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML
location ~* \.html$ {
add_header Cache-Control "public, max-age=0, must-revalidate";
}
# API(公開)
location /api/public/ {
add_header Cache-Control "public, max-age=60, s-maxage=300";
proxy_pass http://backend;
}
# API(ユーザー固有)
location /api/private/ {
add_header Cache-Control "private, no-cache";
proxy_pass http://backend;
}
Cloudflare Workers
export default {
async fetch(request) {
const url = new URL(request.url);
// 静的アセット
if (url.pathname.startsWith('/assets/')) {
const response = await fetch(request);
const headers = new Headers(response.headers);
headers.set('Cache-Control', 'public, max-age=31536000, immutable');
return new Response(response.body, { ...response, headers });
}
// HTML
if (url.pathname.endsWith('.html')) {
const response = await fetch(request);
const headers = new Headers(response.headers);
headers.set('Cache-Control', 'public, max-age=0, must-revalidate');
return new Response(response.body, { ...response, headers });
}
return fetch(request);
}
};
キャッシュ判定フロー
以下のフローチャートでブラウザのキャッシュ判定を理解できます。
リクエスト受信
↓
キャッシュあり?
↓ YES
no-store?
↓ NO
max-age期限内?
↓ YES
キャッシュから返す(200 from cache)
↓ NO
no-cache または must-revalidate?
↓ YES
サーバーに検証(If-None-Match / If-Modified-Since)
↓
304 Not Modified?
↓ YES
キャッシュから返す(304)
↓ NO
新しいレスポンスを返す(200)
stale-while-revalidate の活用
最新のディレクティブで、パフォーマンスと鮮度を両立します。
Cache-Control: max-age=60, stale-while-revalidate=86400
動作:
- 60秒以内: キャッシュから即座に返す
- 60秒〜24時間: 古いキャッシュを即座に返しつつ、裏でサーバーに更新をリクエスト
- 24時間以降: サーバーに問い合わせてから返す
メリット:
- ユーザーは常に高速応答を得られる
- サーバーは適度な頻度で更新を検知できる
ポイント: stale-while-revalidateはCDNでの活用が効果的。エッジで古いキャッシュを返しつつ、オリジンへの問い合わせを非同期化できます。
max-age と s-maxage の使い分け
| ディレクティブ | 対象 | 典型的な値 | 用途 |
|---|---|---|---|
| max-age | ブラウザキャッシュ | 短め(数分〜1時間) | ユーザーが頻繁に更新を確認 |
| s-maxage | CDN/共有キャッシュ | 長め(数時間〜1日) | CDNで長くキャッシュして負荷削減 |
例:
Cache-Control: public, max-age=300, s-maxage=3600
- ブラウザ: 5分キャッシュ
- CDN: 1時間キャッシュ
シナリオ: ニュースサイトの記事一覧。ユーザーは5分ごとに新着をチェックしたいが、CDNは1時間キャッシュして負荷を削減。
デバッグとトラブルシューティング
Chrome DevTools で確認
- Network タブを開く
- リソースをクリック
- Headers タブで
Cache-Controlを確認 - Size 列で
(disk cache)や(memory cache)を確認
キャッシュクリアの方法
# ハードリロード(キャッシュ無視)
Ctrl + Shift + R (Win/Linux)
Cmd + Shift + R (Mac)
# キャッシュとCookie削除
Chrome DevTools → Application → Clear Storage
古いキャッシュを削除したい場合
Cache-Controlは、すでに保存されたレスポンスを一括削除する命令ではありません。確実に更新を反映するには、ハッシュ付きURLへ切り替える、CDNのパージ機能を使う、利用者側でブラウザキャッシュを消す、といった対象別の対応が必要です。Pragma: no-cacheはHTTP/1.0互換の古いリクエスト用ヘッダーで、レスポンスの削除命令にはなりません。
ベストプラクティス
1. リソース種別ごとに最適化
| リソース | Cache-Control | 理由 |
|---|---|---|
| HTML | no-cache | 常に最新を確認 |
| JS/CSS (hash) | max-age=31536000, immutable | URLが変わるまで長期再利用 |
| JS/CSS (no hash) | max-age=3600 | 適度に更新 |
| 画像 | max-age=86400 | 1日キャッシュ |
| API | private, max-age=60 | ユーザー別・短時間 |
2. ハッシュ付きファイル名を採用
# Webpack/Vite等で自動生成
app.a1b2c3.js
style.d4e5f6.css
ファイル名が変わるため、古いキャッシュの心配なし。
3. Vary ヘッダーと組み合わせ
Cache-Control: public, max-age=3600
Vary: Accept-Encoding, Accept-Language
Accept-Encodingや言語ごとに異なるキャッシュを保持。
注意: Expires ヘッダーよりCache-Controlが優先されます。両方あるとCache-Controlが勝ちます。
まとめ
Cache-Controlは、レスポンスの保存先、再利用できる期間、再検証の要否を決めるヘッダーです。まずハッシュ付き静的ファイル、更新されるHTML、ユーザー固有APIを分け、その性質に合わせてpublic/private、max-age/s-maxage、no-cache/no-storeを選びます。immutableやstale-while-revalidateは、更新方法や許容できる古さが明確な場合に使います。
参考リソース
- MDN - Cache-Control
- RFC 9111 - HTTP Caching
- Google - HTTP Caching
- Cloudflare - Cache Control
- MDN - HTTP Conditional Requests