Cache-Control は、クライアント、プロキシサーバー、CDN におけるリソースのキャッシングルールをディレクティブのセットで定義する HTTP ヘッダーです。古い Expires ヘッダーとは異なり、Cache-Control は数十の組み合わせをサポートします:max-age は秒単位でライフタイムを設定し、private と public はキャッシュの利用可能性を制御し、no-cache と no-store は強制的な検証を行います。Google Web Dev (2025)によると、Cache-Control を正しく構成すると、再訪問のページ読み込み時間を50-80%縮減できます。これにより、このヘッダーは Web およびモバイルアプリケーションのパフォーマンスにとって重要です。
メインポイント
Cache-Control は、HTTP/1.1 (RFC 7234) で標準化された HTTP ヘッダーで、クライアント、プロキシ、CDN が応答をどのように、どのくらいの間キャッシュできるかをサーバーが指定できるようにします。Expires (HTTP/1.0) とは異なり、Cache-Control はディレクティブを使用します — カンマで組み合わせたテキストコマンド:Cache-Control: public, max-age=3600, must-revalidate。このヘッダーは、キャッシュチェーンの各リンクを細かく制御できます。
キャッシングは、Web およびモバイルアプリケーションのパフォーマンスの基本的な仕組みの一つです。これがなければ、すべてのユーザーリクエストが直接サーバーに送られ、過剰な負荷と遅延が生じます。Cache-Control は 3 つのキャッシュレベルを定義します:ブラウザ/アプリ (プライベートキャッシュ)、プロキシサーバー (共有キャッシュ)、CDN (分散キャッシュ)。各レベルはディレクティブを異なる形で解釈します。
Cache-Control の設定ミスは、パフォーマンス問題の最もよくある原因の一つです。キャッシュが過剰に効くと、ユーザーは古いデータを見ることになります。キャッシュが不十分だと、サーバーへのリクエストが増え、読み込みが遅くなります。Akamai (2025)によると、静的コンテンツの Cache-Control を最適化すると、サーバー負荷が 70-90% 減少し、モバイルユーザーの読み込み時間が 40-60% 改善されます。
Cache-Control は HTTP/1.1 (RFC 2616, 1999) で Expires の置き換えとして登場しました。Expires には根本的な問題がありました:絶対日付を使用しており、サーバーとクライアントのタイムゾーンに依存していました。Cache-Control は相対時間(応答を受信した瞬間からの秒単位の max-age)に切り替えることでこの問題を解決しました。その後、RFC 7234 (2014) で、新しいディレクティブが追加されました:静的アセット用の immutable、遅延検証用の stale-while-revalidate と stale-if-error。
Cache-Control には 10 以上のディレクティブがあり、リクエストディレクティブ、応答ディレクティブ、拡張の 3 つのグループに分けられています。実際には、モバイル開発では 6~7 つの主な応答ディレクティブを使用し、95% のキャッシングシナリオをカバーします。例と提言を交えて、それぞれを見てみましょう。
| ディレクティブ | 意味 | 例 |
|---|---|---|
| max-age | 応答からの秒単位のライフタイム | max-age=3600 — 1時間 |
| s-maxage | 共有キャッシュの max-age(プロキシ、CDN) | s-maxage=86400 — CDN で 1日 |
| public | すべてのキャッシュを許可(プロキシを含む) | public, max-age=3600 |
| private | ブラウザ/アプリのみキャッシュを許可 | private, max-age=600 |
| no-cache | 検証なしで使用しない(304 が必要) | no-cache |
| no-store | キャッシングを完全に禁止 | no-store |
| must-revalidate | max-age 後にオリジンで再検証する必要がある | max-age=3600, must-revalidate |
| immutable | リソースは変わらない(バージョン決め込み静的アセット用) | max-age=31536000, immutable |
max-age は最も重要なディレクティブです。指定した時間、クライアントがサーバーにリクエストを送るのを禁止します。静的アセット(CSS、JS、画像)の場合、max-age は通常 1日から 1年に設定されます。API 応答の場合は、0秒(常に最新データ)から 5~10分(参考データ)です。s-maxage は CDN とブラウザで異なるライフタイムを設定できます:CDN は 1日間コピーを保持し、ブラウザは 1時間保持します。
これら 2 つのディレクティブはよく混同されます。no-cache はキャッシングを禁止するわけではありません — 条件付きリクエスト(If-Modified-Since または If-None-Match)を介して、使用ごとにキャッシュされたコピーを検証する必要があります。サーバーが 304 で応答した場合はキャッシュを使用し、200 の場合は更新します。no-store は、ディスクやメモリを含むすべてのキャッシュに応答を保存することを完全に禁止します。機密データ(トークン、支払い情報、個人文書)にのみ no-store を使用してください。
Expires ヘッダー(HTTP/1.0)もリソースのライフタイムを指定しますが、絶対日付を使用します:Expires: Thu, 03 Jul 2026 12:00:00 GMT。Cache-Control max-age は応答からの相対時間を使用します。この違いは分散システムにとって重要です。サーバーとクライアントのタイムゾーンが違う場合、Expires は正しく解釈されない可能性があります。Cache-Control にはこの問題はありません — 3600 秒は常に 3600 秒です。
両方のヘッダーがある場合、Expires よりも Cache-Control が優先されます。これは RFC 7234 で定義されています:「応答に max-age ディレクティブを含む Cache-Control フィールドがある場合、受信者は Expires フィールドを無視しなければなりません。」実際には、現代のクライアントに対して Expires を全く返さないことが推奨されます。Cache-Control がすべての Expires シナリオをカバーするからです。ただし、古いプロキシやブラウザとの互換性のために、両方のヘッダーを返すこともできます。
Expires は主に Nginx や Apache の静的コンテンツとして残っています — これらのサーバーは自動的に両ヘッダーを追加します。プロジェクトで Cache-Control なしに Expires があった場合、max-age を使った Cache-Control に置き換えてください:キャッシュ制御の精度が向上し、タイムゾーンへの依存が解消されます。移行には、Expires の代わりに Cache-Control を追加するようにサーバーを構成すれば十分です。
# Nginx: 静的ファイルのための Cache-Control
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, immutable, max-age=2592000";
}
# 異なるコンテンツタイプに対する異なるポリシー
location /api/config {
expires -1;
add_header Cache-Control "no-cache, must-revalidate";
}
location /api/static-data {
expires 5m;
add_header Cache-Control "public, max-age=300";
}
Nginx の構成では、静的ファイル(CSS、JS、画像)は immutable 属性で 30 日間の Cache-Control に設定されます — この属性は、この URL でリソースが変わらないことをブラウザに伝えます(ファイル名のハッシュによるバージョン管理)。API エンドポイントは、動的データには no-cache、参考データ(よくリクエストされるがほとんど変化しないリスト)には短い max-age の public を使用します。
モバイルアプリでは、モバイルネットワークの制約(高い遅延、不安定な接続、トラフィック制限)のため、Cache-Control は特別な役割を果たします。適切なキャッシングにより、オフラインでもデータを即座に表示し、バックグラウンドで更新できます。Android の OkHttp や iOS の URLSession には、Cache-Control を尊重する組み込みキャッシングシステムがあります。
OkHttp は CacheInterceptor を使用し、応答から Cache-Control を読み取って自動的にキャッシングを管理します。サーバーが Cache-Control: max-age=3600 を返した場合、OkHttp は 1 時間サーバーにリクエストを送りません。max-age が切れた後、OkHttp は If-Modified-Since と If-None-Match を使った条件付きリクエストを送ります。OkHttp のキャッシュ構成:OkHttpClient.Builder().cache(Cache(directory, maxSize))。
fun createCachedClient(cacheDir: File): OkHttpClient {
return OkHttpClient.Builder()
.cache(Cache(cacheDir, 10L * 1024 * 1024))
.addNetworkInterceptor { chain ->
val response = chain.proceed(chain.request())
response.newBuilder()
.header("Cache-Control",
"public, max-age=300")
.removeHeader("Pragma")
.build()
}
.build()
}
このコードは 10 MB のキャッシュを持つ OkHttpClient を作成し、NetworkInterceptor を介して Cache-Control を上書きします。サーバーが Cache-Control を返さなかったり、Expires を使用している場合、インタセプターは public, max-age=300 (5分) を追加します。インタセプターは、互換性のために古い Pragma ヘッダー(HTTP/1.0)を削除します。iOS のキャッシングは、memoryCapacity と diskCapacity の設定を持つ URLCache.shared を介して同様に動作します。
stale-while-revalidate ディレクティブは、アプリがバックグラウンドで新しいデータを取得している間、ユーザーに古いキャッシュを表示することができます。これにより即座レスポンス効果が得られます:ユーザーはすぐにコンテンツを見て、数秒後に最新バージョンに更新されます。OkHttp はバージョン 3.10 から、URLCache は iOS 14+ でサポートされています。例:Cache-Control: max-age=3600, stale-while-revalidate=300 — 1時間の最新キャッシュ、その後 5分間はバックグラウンド更新で古いデータを表示。
異なるリソースタイプには、異なるキャッシング策略が必要です。モバイル開発の典型的なシナリオに対する最適な構成を見てみましょう。ファイル名にハッシュがある静的コンテンツ(bundle.abc123.js)の場合、max-age を immutable 付きで 1 年まで設定できます。あまり更新されない API リスト(ディレクトリ、カテゴリ)の場合は、stale-while-revalidate 付きで 5分から 1時間の max-age です。
| リソースタイプ | Cache-Control | 説明 |
|---|---|---|
| バージョン決め込み静的アセット | public, max-age=31536000, immutable | 1年、ファイルは変わらない(URL にハッシュ) |
| 非バージョン静的アセット | public, max-age=86400, must-revalidate | 後に強制再検証付き 1日 |
| API: 参考データ | public, max-age=600, stale-while-revalidate=60 | 10分キャッシュ + 1分古いデータ |
| API: ユーザーデータ | private, max-age=60 | 1分、特定のユーザーのみ |
| API: 機密データ | no-store | キャッシュ完全禁止 |
| HTML ページ | no-cache, must-revalidate | リクエストごとに検証、変化なければ304 |
セキュリティを忘れないでください:個人データを含む応答には常に private を設定してください。このディレクティブがなければ、公共プロキシ(例:企業用)が応答をキャッシュして、他のユーザーに届ける可能性があります。認証トークンや支払い情報には no-store を使用してください — プライベートキャッシュでもディスクにこれらのデータを保存してはいけません。
Cache-Control の正確さを確認するには、Age ヘッダー(キャッシュが保存されていた秒数)と X-Cache(CDN のヒット/ミス)を使用します。ブラウザでは Network タブの Size 列に「from disk cache」または「304 Not Modified」と表示されます。キャッシュされるべきリソースが毎回読み込まれる場合、サーバーがあなたのディレクティブとともに Cache-Control: no-cache または Pragma: no-cache を追加していないか確認してください。
よくある質問
max-age はすべてのキャッシュ(ブラウザを含む)に適用され、s-maxage は共有キャッシュ(プロキシ、CDN)のみに適用されます。s-maxage が指定されている場合、CDN は max-age を無視して s-maxage を使用します。これにより、ブラウザと CDN で異なるライフタイムを設定できます。
いいえ、max-age 付きの応答を送信した後、タイマーが切れるまでクライアントはリクエストを送りません。即座にキャッシュを無効化するには、リソースの URL を変更し(バージョン/ハッシュを追加)、プッシュ通知または WebSocket メッセージ を送信して強制リセットする必要があります。
immutable ディレクティブ (RFC 8246) は、この URL でリソースが絶対に変わらないことをブラウザに伝えます。ページ更新時にブラウザは条件付きリクエストを実行しようとしません — max-age が切れるまでキャッシュを使用します。バージョン決め込みファイルだけで動作します。
Googlebot は Cache-Control を考慮します:長いキャッシングは再クローリングを加速します。高速キャッシュの noindex は問題ありません。no-store は、Googlebot が毎回ページをゼロから読み込むため、インデックスを遅くする可能性があります。max-age が短すぎると、クローリング時のサーバー負荷が増します。
helmet またはミドルウェアを介して:res.set('Cache-Control', 'public, max-age=3600')。静的ファイルには、maxAge パラメーターを使った express.static を使用します:express.static('public', {maxAge: '1y'})。動的ルートには 各ハンドラーで個別に設定します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。