HTTPキャッシュの理解：強制キャッシュと協調キャッシュ

[![表紙](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/0205315d-67fa-4396-8116-31ad4a34a1d5.png)](https://leapcell.io/?lc_t=q_cachemechanism)

## ブラウザのキャッシュメカニズム

私たちは通常、ブラウザでページを開く際、入力した URL に基づいて対応するサーバーにデータリソースをリクエストします。しかし、このプロセスではページがレンダリングされるまでに一定の待ち時間（ホワイトスクリーンタイム）が発生することがあります。

ユーザーエクスペリエンスを向上させたい場合、さまざまなキャッシュ技術が欠かせません。例えば、DNS キャッシュ、CDN キャッシュ、ブラウザキャッシュ、ページのローカルキャッシュなどがあります。適切なキャッシュ戦略を導入することで、重複したリソースリクエストを減らし、サーバーの負荷を軽減し、ページのロード速度を向上させることができます。

本記事では、HTTP の**強制キャッシュ**と**協調キャッシュ**について説明します。

## 基本原理

ブラウザがリソースを読み込む際、最初にリクエストヘッダーの `Expires` や `Cache-Control` の値を基に、強制キャッシュが適用されるかどうかを判断します。キャッシュが有効であれば、リモートサーバーにリクエストせずにローカルキャッシュを使用します。

### 強制キャッシュ（強キャッシュ）

ブラウザの強制キャッシュには、`Expires`（HTTP/1.0 規格）と `Cache-Control`（HTTP/1.1 規格）の 2 種類があります。

#### Expires（有効期限）

`Expires` は HTTP/1.0 の規格で、リソースの有効期限を示すヘッダーフィールドです。この値はサーバー側で設定され、絶対時間で指定されます。

ブラウザが最初にリソースをリクエストすると、サーバーのレスポンスヘッダーに `Expires` が含まれます。その後、ブラウザが同じリソースを再リクエストすると、前回の `Expires` の値を確認し、まだ有効期限内であればキャッシュを使用します。

ただし、`Expires` は**クライアントのローカル時間**を基準にするため、クライアントとサーバーの時間がずれている場合、キャッシュの判定に誤差が生じる可能性があります。

#### Cache-Control（キャッシュ制御）

上記の `Expires` には、クライアントとサーバーの時間のズレによる誤差という欠点があります。これを解決するために、HTTP/1.1 では `Cache-Control` が導入されました。`Cache-Control` の優先度は `Expires` より高く、キャッシュの有効期間を**相対時間**で指定できます。

`Cache-Control` には以下のような主要なディレクティブ（指示）が含まれます：

- `max-age`：例えば `max-age=3600` の場合、**現在の時間＋ 3600 秒**の間、新しいリソースをリクエストしません。
- `s-maxage`：`max-age` に似ていますが、**プロキシサーバーのキャッシュ時間**を指定するためのものです。
- `private`：キャッシュを**クライアント側のみに**保存し、プロキシサーバーには保存しません。
- `public`：クライアントとプロキシサーバー**両方**がキャッシュ可能。
- `no-store`：データを**一切キャッシュしない**。
- `no-cache`：**キャッシュには保存するが、リソースを使用する前に必ずオリジンサーバーで検証する**。

### 協調キャッシュ（協商キャッシュ）

強制キャッシュはローカルブラウザ側でキャッシュを管理しますが、**キャッシュが無効になった場合**はサーバーへリクエストを送信し、**協調キャッシュ**の判定を行います。もしキャッシュが有効ならば、**HTTP ステータスコード 304**（Not Modified）が返され、新しいリソースデータは送信されません。

協調キャッシュ（**比較キャッシュ**とも呼ばれる）は、**サーバー側でリソースの有効性を判定**する仕組みです。これには、以下の 2 つのフィールドペアが使用されます。

1. `Last-Modified` / `If-Modified-Since`
2. `ETag` / `If-None-Match`

初回リクエスト時に、サーバーは `Last-Modified` または `ETag` をレスポンスヘッダーに含めます。次回のリクエスト時には、ブラウザは以下のリクエストヘッダーを送信します。

- `Last-Modified`：リソースの**最終更新日時**をサーバーが返す。
- `If-Modified-Since`：クライアントが以前受信した `Last-Modified` の値をサーバーに送信し、それが変更されていないか確認。
- `ETag`：ファイルの**一意の識別子**（ハッシュ値など）。リソースの内容が変更されると、新しい `ETag` に更新される。
- `If-None-Match`：前回受信した `ETag` をリクエストヘッダーに含め、現在のリソースと一致するか確認。

サーバーは `ETag` または `If-Modified-Since` の値をチェックし、変更がなければ**304 Not Modified**を返し、クライアントはローカルキャッシュを使用します。

## 強制キャッシュと協調キャッシュの具体的なフロー

1. **ブラウザがリソースをリクエスト**

   - ローカルにキャッシュがなければ、サーバーから新しいリソースを取得し、`Last-Modified` や `ETag` を保存。

2. **キャッシュが存在する場合**

   - **強制キャッシュ（`Cache-Control` > `Expires`）の判定**
   - 有効期間内ならローカルキャッシュを使用（ステータスコード **200**）。
   - 期限切れの場合、協調キャッシュの検証を実施。

3. **協調キャッシュの検証**
   - `ETag` が存在し、サーバーの `ETag` と一致すれば、**304 Not Modified** を返し、キャッシュを再利用。
   - `ETag` がなければ、`If-Modified-Since` と `Last-Modified` を比較し、一致すれば **304 Not Modified** を返す。
   - `ETag` や `Last-Modified` が不一致なら、新しいリソースを取得。

### なぜ `ETag` が必要なのか？

`ETag` は、`Last-Modified` のいくつかの問題を解決するために導入されました。

- **ファイル内容が変更されていないのに最終更新日時が変わることがある**
  - 例えば、サーバー上でファイルをコピー＆ペーストした場合、ファイルの更新日時が変わる可能性があります。これにより、クライアントは「リソースが変更された」と誤認し、不要なリクエストが発生します。
- **ファイルの変更が頻繁に行われる場合、`Last-Modified` の秒単位の精度では対応できない**

  - `If-Modified-Since` は**秒単位**でしか変更を検知できないため、1 秒以内に複数回変更が発生する場合、正しく判定できません。
  - `ETag` なら、より細かい変更の追跡が可能です。

- **一部のサーバーではファイルの最終更新日時を正確に取得できない**
  - 例えば、動的に生成されるファイルなどでは、`Last-Modified` の管理が難しい場合があります。`ETag` を使えば、サーバー側でリソースの一意性を保証できます。

### ステータスコードの違い

- **200**：通常の成功レスポンス。サーバーが新しいリソースを返す。
- **200 (from memory cache / from disk cache)**：強制キャッシュが有効な場合。ブラウザのローカルキャッシュを使用する。
- **304 Not Modified**：協調キャッシュが適用され、リソースの再取得が不要と判定された場合。

**補足**：

- `from memory cache` はページのリフレッシュ時に**メモリから**取得されたことを示す。
- `from disk cache` はタブを閉じた後、**ディスクキャッシュから**取得されたことを示す。

## キャッシュの優先順位

キャッシュの適用順序は以下のようになります。

1. **`Cache-Control` > `Expires`**

   - `Cache-Control` がある場合、`Expires` は無視される。
   - つまり、**`Cache-Control` の方が優先度が高い**。

2. **強制キャッシュ > 協調キャッシュ**

   - **強制キャッシュが有効な場合、協調キャッシュの検証は行われない**。
   - つまり、**まず強制キャッシュを確認し、期限切れなら協調キャッシュを適用する**。

3. **`ETag` > `Last-Modified`**
   - 協調キャッシュの判定時、`ETag` がある場合は `Last-Modified` を無視する。
   - つまり、**`ETag` の方が優先度が高い**。

### 追加情報

HTTP/1.0 では、`Pragma` というキャッシュ制御ヘッダーが使用されていました。これは `Cache-Control: no-cache` と同じ効果を持ち、リソースを再検証するよう強制します。しかし、HTTP/1.1 では `Cache-Control` が導入され、`Pragma` は**ほぼ廃止**されました。

キャッシュ適用の優先順位は以下のようになります：

```
Pragma → Cache-Control → Expires → ETag → Last-Modified
```

## ヒューリスティックキャッシュ（推測キャッシュ）

サーバーが `Cache-Control` や `Expires` を設定していない場合、ブラウザは**ヒューリスティックキャッシュ**という独自のキャッシュ戦略を適用することがあります。これは、`Last-Modified` の値を基にキャッシュの有効期間を推測する方式です。

例えば、以下のような計算が適用されることがあります：

```
(現在の時刻 - Last-Modified の時刻) × 0.1
```

つまり、リソースが更新されてから 10%の期間をキャッシュ有効期間として推測する、というルールです。

ただし、この戦略はサーバーのキャッシュポリシーに依存せず、ブラウザの独自の判断で行われるため、一貫性に欠けることがあります。

参考：
[HTTP Heuristic Caching (Missing Cache-Control and Expires Headers) Explained](https://paulcalvano.com/2018-03-14-http-heuristic-caching-missing-cache-control-and-expires-headers-explained/)

## その他の補足

- **協調キャッシュは強制キャッシュと組み合わせて使用するのが基本**

  - 強制キャッシュを無効にしてしまうと、協調キャッシュだけではあまり意味がありません。

- **ほとんどの Web サーバーでは、デフォルトで協調キャッシュ（`Last-Modified` と `ETag`）が有効になっている**
  - つまり、特に設定しなくても協調キャッシュが機能している場合が多い。

## 注意すべきケース

### 1. **分散システム（クラスタリング環境）での `Last-Modified` の一貫性**

- `Last-Modified` の値が**異なるサーバー間で統一されていないと、キャッシュが適切に機能しない**可能性があります。
- 例えば、異なるサーバーでレスポンスを処理する際、最終更新日時がバラバラだと、ブラウザは**毎回キャッシュを無効化して新規リクエストを送信する**ことになります。

### 2. **分散システムでは `ETag` の使用を避けるべき**

- `ETag` はサーバーごとに異なる値が生成されるため、**負荷分散環境ではキャッシュが正しく適用されない**ことがあります。
- そのため、クラスタリング環境では **`ETag` を無効化するのが一般的**です。

---

### [私たちはLeapcell、バックエンド・プロジェクトのホスティングの最適解です。](https://leapcell.io/?lc_t=q_cachemechanism)

[![Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/097151fc-e519-45fe-a762-8bdebef0fd9a.png)](https://leapcell.io/?lc_t=q_cachemechanism)

[Leapcell](https://leapcell.io/?lc_t=q_cachemechanism)は、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです：

**複数言語サポート**

- Node.js、Python、Go、Rustで開発できます。

**無制限のプロジェクトデプロイ**

- 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

**比類のないコスト効率**

- 使用量に応じた支払い、アイドル時間は課金されません。
- 例: $25で6.94Mリクエスト、平均応答時間60ms。

**洗練された開発者体験**

- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

**簡単なスケーラビリティと高パフォーマンス**

- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。

[ドキュメントで詳細を確認！](https://docs.leapcell.io/)

[![Try Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/f38acf76-9fa5-4ac8-94e8-52c709ae260b.png)](https://leapcell.io/?lc_t=q_cachemechanism)

Xでフォローする：[@LeapcellHQ](https://x.com/LeapcellHQ)

---

[ブログでこの記事を読む](https://leapcell.io/blog/http-caching-explained)