Webのキャッシュ制御を理解する

## はじめに

「キャッシュが残ってて最新のデータが見えない」ことや「ユーザごとに違うはずのページが他の人に見えてしまった」など，キャッシュによる意図しない挙動に頭を悩ませた経験があったため簡単にまとめてみました．

この記事では，[IIJ Engineers Blogの解説記事](https://eng-blog.iij.ad.jp/archives/18584)をベースに，HTTPのキャッシュ制御とプロキシの役割について整理しています．こちらの記事は図などもおおく非常にわかりやすいのでおすすめです！（その分ボリュームがあるため，短いバージョンを本記事で作成します）


## 用語の整理
| 用語 | 説明 |
|------|------|
| **ブラウザ** | Webページを見るためのアプリケーション |
| **Webサーバ** | HTMLなどのコンテンツを提供するサーバ |
| **HTTPクライアント** | Webサーバにアクセスする側．今回はブラウザ |
| **プロキシ** | 通信を中継・代理するサーバ．通信の制御やキャッシュで使用 |
| **フォワードプロキシ** | クライアントの代理．社内LAN → インターネットなど．内→外 |
| **リバースプロキシ** | サーバの代理．CDNやロードバランサが該当．外→内 |



## キャッシュの基本
現代のWebページはリッチ化の一途をたどっており，通信速度が高速になったとしてもリソースを逐一ダウンロードしていては時間がかかってしまいます．それを防ぐために，すでにダウンロード済みのものを利用するという高速化技術を**キャッシュ**と言います．これはブラウザやプロキシ等で行われます．
さらにキャッシュにリソースを保持しておく時間的な長さのことを**キャッシュ期間**といいます．

---

ここで，キャッシュに存在するトレードオフを紹介します．
### キャッシュ期間を長くすると... 
- ✅ 重複したリソースのダウンロードやWebサーバとの通信量が減り，表示が速くなる
- ❌ 古いデータが残ってしまう可能性がある

### キャッシュ期間を短くすると...
- ✅ 常に最新の情報を取得できる
- ❌ 通信が増えたり，キャッシュの意味がなくなる


理想としては，リソースが更新された時だけダウンロードを行い，キャッシュを常に新しい状態に保つことですよね．
これを実現するために行われるのが「**キャッシュの検証**」です．


## キャッシュの検証と `304 Not Modified`

キャッシュの検証は，**ブラウザがサーバに「このキャッシュ，まだ使っていい？」と確認する仕組み**です．

### サーバの返事が「変更なし」なら...
- ステータスコード `304 Not Modified` が返る
- ブラウザはキャッシュをそのまま再利用
- 同じデータを再度ダウンロードせず，効率的！



### 304応答の仕組み：2つの方式

#### 1. 更新時間での検証：`If-Modified-Since` ヘッダ

- サーバ側: `Last-Modified` ヘッダで最終更新日時を返す
- ブラウザ側: 次回リクエスト時の `If-Modified-Since` に最終更新日時を付ける
- 比較結果: 変更がなければ `304`，あれば新データを送信

#### 2. 識別子（ETag）での検証：`If-None-Match` ヘッダ

- サーバ側: `ETag` ヘッダで識別子を返す
- ブラウザ側: 次回リクエスト時の `If-None-Match` に`ETag` ヘッダを付ける
- 一致すれば `304`，不一致なら新しいデータを送る

> `If-Modified-Since` と `If-None-Match` の両方がある場合は，ETagが優先されます！
> 柔軟に(広告とブログ間でキャッシュ期間を分けるみたいな)一致条件を制御できるため


## プロキシとキャッシュの注意点

### ブラウザキャッシュとプロキシキャッシュは別物
キャッシュは基本的にブラウザとプロキシで行われます．この2つは同じに見えますが，　別物として管理する必要があります．
なぜならプロキシキャッシュは複数のユーザに利用されることが多いからです．

- ブラウザキャッシュは**個人利用**で，利用者本人が管理
- プロキシキャッシュは**複数人利用**で，プロキシ管理者が管理


例えばクレジットカード番号や認証後のマイページなど，**他人に見られたくないページ**はプロキシにキャッシュさせてはいけません！

したがってキャッシュをどこで扱うかコントロールする必要があります．
それが`Cache-Control`ヘッダです．


## `Cache-Control` ヘッダの基本

キャッシュの挙動は，`Cache-Control` ヘッダで細かく指定できます．
主にリクエスト時とレスポンス時の2種類に分けられます．

### リクエスト時の指定
ブラウザからサーバへリクエストを送る際，すでに保存されているキャッシュを使うかどうか，クライアント側が指定できます！
特にCSSなどが新しく反映されないときにスーパーリロードを使って最新データを強制取得する動作などで使われます．

| ディレクティブ | 説明 |
|------|------|
| `no-cache` | 検証はするがキャッシュは信用しない（`max-age=0`と同等） |
| `max-age=60` | 60秒以内のキャッシュは使ってもよい |

#### `no-cache`はキャッシュを使わないって意味じゃないの？
よく勘違いしがちなのですが，`no-cache`は（正確には）キャッシュを使わないって意味ではありません．

1.リクエスト時はキャッシュを利用せず，Webサーバへ問い合わせます
2-1. レスポンスが`200`であり，キャッシュに保存されているものよりも新しいリソースに更新されている場合はそれをレスポンスとして返します
2-2. **レスポンスが`304`ならキャッシュに保存されているものを利用するのです**


:::note info
一切キャッシュを利用したくなければ`no-store`を使いましょう
:::


### レスポンス時の指定
サーバからブラウザへレスポンスを返す際，かなり複雑で数も多く，状況によってディレクティブの意味が変わります．そのため，キャッシュの再利用に関わる動作だけに注目して解説します．


| 指定 | 対象 | 説明 |
|------|------|------|
| `no-store` | ブラウザとプロキシ | 一切キャッシュさせない（履歴にも残さない） |
| `private` | プロキシ | ユーザ単位のキャッシュのみ許可（プロキシには不可） |
| `public` | プロキシ | 誰が見ても問題ない内容．プロキシ共有可 |
| `must-revalidate` | ブラウザとプロキシ | キャッシュが切れたら再検証必須 |
| `max-age=300` | ブラウザとプロキシ | キャッシュの有効期限（秒） |
| `s-maxage=600` | プロキシ | プロキシ用キャッシュの有効期限 |


#### キャッシュの再利用が決定される時
1. 既存キャッシュに対して，キャッシュ更新の検証は必要か
2. 検証が不要な場合，キャッシュは新鮮か
3. 接続失敗時，キャッシュを使用するか




**「検証は必要か？」**
- **「検証する」＝ キャッシュは使うが，サーバに確認をとる（If-Modified-Since や ETag）**
- `no-cache` や `no-store` があると検証が強制される
- キャッシュが更新されて，最新データが表示される
- つまりキャッシュを利用していないことと同等




**「キャッシュは新鮮か？」**
- キャッシュ期間を超えて古くなっていないかを，サーバ接続前に判断する処理(検証なしでキャッシュを活用するか判断する処理)





**「接続失敗時，キャッシュを利用するか？」**
- 認証失敗もキャッシュ利用してしまうので，情報漏洩につながる危険性あり
- no-storeなどを利用して強制的にキャッシュさせないことも必要


## 具体例：キャッシュしつつ安全に情報を扱いたい場合

### ユースケース
- 会員マイページのように**更新頻度は低い**
- **ブラウザにはキャッシュしてほしい**
- **プロキシにはキャッシュしてほしくない**
- **認証失敗時に古い情報を出したくない**

### 推奨ヘッダ
```http
Cache-Control: max-age=300, private, must-revalidate
```

## 最後に
キャッシュについてまとめました．プロキシやWebブラウザでキャッシュが行われていることはなんとなくわかっていたつもりでしたが，キャッシュ制御の必要性を考慮する必要があるということは目から鱗でした．
HTTPには他にも様々な高速化技術が組み込まれているため今後も勉強していきます．


## 参考
https://eng-blog.iij.ad.jp/archives/18584


https://www.oreilly.co.jp/books/9784814400669/