この記事の要点
• Cache-ControlはブラウザとCDNのキャッシュ動作を制御するHTTPヘッダー
• max-ageでキャッシュ有効期限を秒単位で指定
• no-cacheは「キャッシュしない」ではなく「毎回検証する」の意味
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 | CDN含め誰でもキャッシュ可能 | 静的な公開リソース |
| private | ブラウザのみキャッシュ可能(CDN不可) | ユーザー固有のデータ |
| no-cache | キャッシュするが毎回検証必須 | 常に最新を確認したいリソース |
| no-store | 一切キャッシュしない | 機密情報、個人情報 |
有効期限の制御
| ディレクティブ | 説明 | 例 |
|---|---|---|
| max-age=N | ブラウザ用の有効期限(秒) | max-age=3600 (1時間) |
| s-maxage=N | CDN/共有キャッシュ用の有効期限 | s-maxage=86400 (24時間) |
| immutable | 期限内は絶対に変更されない | max-age=31536000, immutable |
再検証の制御
| ディレクティブ | 説明 | 用途 |
|---|---|---|
| must-revalidate | 期限切れ後は必ず再検証 | 金融データ、在庫情報 |
| proxy-revalidate | CDN向けのmust-revalidate | CDNだけ厳密にしたい場合 |
| stale-while-revalidate=N | 期限切れ後N秒は古いキャッシュを返しつつ裏で更新 | 高速応答と鮮度の両立 |
| stale-if-error=N | エラー時は期限切れキャッシュをN秒まで利用 | 可用性重視 |
実践メモ: immutableはハッシュ付きファイル名(例: `app.a1b2c3.js`)専用です。リロード時の不要な検証を完全に防げます。
よく使うパターン
静的アセット(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
- 理由: キャッシュを一切残さない。
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レスポンスも使わない
用途: 機密情報、個人情報、銀行の残高など。
実装例
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: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0
PragmaとExpiresは古いブラウザ対応です。
ベストプラクティス
1. リソース種別ごとに最適化
| リソース | Cache-Control | 理由 |
|---|---|---|
| HTML | no-cache | 常に最新を確認 |
| JS/CSS (hash) | max-age=31536000, immutable | 永久キャッシュ |
| 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は、Webパフォーマンス最適化の最重要ヘッダーです。リソースの性質に応じてpublic/private、max-age/s-maxage、no-cache/no-storeを使い分けることで、高速なユーザー体験とサーバー負荷削減を両立できます。特にimmutableとstale-while-revalidateは、モダンなWebサイトで積極的に活用すべきディレクティブです。
参考リソース
- MDN - Cache-Control
- RFC 9111 - HTTP Caching
- Google - HTTP Caching
- Cloudflare - Cache Control
- MDN - HTTP Conditional Requests