キャッシュ導入に向けて知っておくべきことをまとめてみた

こんにちは、@inagakkieです。この記事は、[Livesense not engineers Advent Calendar 2018](https://qiita.com/advent-calendar/2018/livesense-not-engineers)の18日目の投稿です。
ご本人匿名希望のため、本記事限定投稿で代理でお届けします＾＾

-------

UXやSEOの向上のためにサイトの速度を改善する議論がよく発生します。
「完全に表示されるまでに3秒以上かかると、53%のユーザーはページを離れる」「表示速度が1秒から7秒に落ちると、直帰率は113%上昇」といったGoogleの調査結果[^1]も公表されているなど、サイト速度の向上はサービス運営において重要な要素になってきています。
[^1]: https://www.thinkwithgoogle.com/marketing-resources/data-measurement/mobile-page-speed-new-industry-benchmarks/

サイト速度を向上させる議論の上で度々登場するのがキャッシュを入れるという議論ですが、今回はそのキャッシュが一体どういうもので、何が出来て何に注意する必要があるのか、何を考えて導入するべきなのかについて調べてみたのでまとめました。

## そもそもキャッシュとは
キャッシュとは、一度利用したデータを再度活用して高速にアクセス・利用できるようにするための技術です。
利用するデータが不変であれば非常に効率よくアクセス・利用ができます。
一方で、改変が定期的に行われる場合はその改変に追従してキャッシュを新しくするという作業が必要になり、それが頻繁であれば逆に効率の悪化にもつながったり、利用者によってみせるデータが変わるケースにおいてはキャッシュをしないなど細かな対応が必要ケースも存在します。
キャッシュを利用する際は戦略をもってきちんと利用すればより効率的にサイト運営ができるようになります。

## キャッシュの種類について
サイト運営において登場するキャッシュは大きく分けて以下のように分類されます。

- Webブラウザによるキャッシュ
  - ブラウザキャッシュ
- プロキシ等によるキャッシュ
  - Webサーバーによるコンテンツキャッシュ
  - CDNによるコンテンツキャッシュ
- アプリケーションによるキャッシュ
  - キャッシュ専用のデータストアに格納することでできるキャッシュ
- ツールの機能によるキャッシュ
  - データベースのクエリキャッシュ

今回は「Webブラウザによるキャッシュ」と「プロキシ等によるキャッシュ」についてまとめてみました。

### Webブラウザによるキャッシュ
Webブラウザが持つキャッシュ機能で一般に「ブラウザキャッシュ」と呼ばれています。
ページの表示がおかしい際にスーパーリロード(`Shift+F5`や`Command+Shift+R`)すればおかしいのが治る場合の主な原因になっているものです。
世の中の多くのWebページは複数のファイル(HTML、CSS、JavaScript、画像など)によって構成されており、ページを正しく表示するためにはすべてのファイルをインターネット越しで取得する必要があります。
その際以下のようなことが課題になってきます。

- 取得してくるファイルのサイズが大きい場合、低速のインターネット回線(またはモバイル回線)では表示に時間がかかる
- 取得してくるファイルのサイズが大きい場合、モバイル回線のデータ容量に大きく消費してしまう
- 各ファイルはサーバーに対して個別の要求を行うため、同時にリクエストを処理する必要があり負荷の増加・ページ速度の低下につながる

ブラウザキャッシュはこれらのファイルの一部を一時的にユーザーのブラウザにローカル保存します。
そうすることで初回アクセスは同様に時間がかかるものの再度アクセスした場合や次のページで同様のファイルが必要になった場合はインターネット越しに取得せず、ローカル保存したキャッシュを自動的に使い回す動作をしてくれます。
こうすることで上記のような課題を解決してくれる仕組みです。
ブラウザキャッシュはHTTPのヘッダーの情報を元にブラウザ側が自動でキャッシュをしてくれるのでアプリケーション側で時間や対象をコントロールする必要があります。

### プロキシ等によるキャッシュ
最近話題のCDNなどのプロキシ層で行うコンテンツそのもののキャッシュを行う仕組みです。
今回は便宜上「コンテンツキャッシュ」と呼称します。
ブラウザキャッシュが各ユーザー個々が初回にアクセスした場合にキャッシュを作成、以降使い回すのに対して、コンテンツキャッシュはユーザー全体でキャッシュを作成し、使いまわします。
サーバーの末端(オリジン)で作成されたコンテンツをよりユーザーに近い場所(CDNやロードバランサーなどプロキシ)でキャッシュすることで高速化を目指します。
コンテンツそのものをキャッシュすることでオリジンで毎回作成されることがなくなるため、作成にかかる時間・負荷を削減できたり、CDNは物理的にユーザーに近いところにキャッシュを持つことができるため、より高速にユーザーにコンテンツを届ける事ができます。
コンテンツキャッシュはCDNやロードバランサーのミドルウェア等でキャッシュ時間や対象をコントロールしたり、HTTPのヘッダーを元にアプリケーション側でコントロールする必要があります。

## ブラウザキャッシュ戦略
ブラウザキャッシュを設定する際に考慮するポイントについてまとめてみました。

### 対象
ブラウザキャッシュはユーザー側にキャッシュがあるため、コントロールが効きにくいので静的なコンテンツに限定した方が良いようです。

- 静的なコンテンツ
    - プログラム等で動的に生成されていないHTML
    - CSS
    - JavaScript
    - フォントファイル
    - テキストファイル(.txt、.xml、etc...)
    - 画像
    - 動画
    - etc...

### 条件
- キャッシュはURLごとに行われる
    - Ex: https://hoge.jp/fuga/piyo?foo=bar
- Expiresヘッダーで設定する
    - キャッシュの有効期限を設定する
    - Ex: `Expires: Mon, 24 Dec 2018 05:54:13 GMT`
- Cache-Controlヘッダーで設定する
    - Ex: `Cache-Control: public, max-age=31536000`
- 最もキャッシュ効率を上げるならば同一URLにおいてコンテンツを不変にすればいい
    - コンテンツを変更する場合はURLを変える
        - キャッシュバスター
            - クエリパラメーターでリリースごとにハッシュ値を変更することでURLを変更する
        - コンテンツを変えた場合はURLを変える
            - URLをハッシュ値等にする
    - 頻繁に変更する部分はファイルを分けておくと便利
        - CSS、JavaScript等

### 期間
推奨の期間は条件や環境によって異なるようです。
以下は公開されている参考値です。

#### Google PageSpeedにおける推奨[^2]
- 完全な静的コンテンツ(URLを不変にできている場合)
    - 1年
- その他の静的コンテンツ
    - 1週間

[^2]: https://developers.google.com/speed/docs/insights/LeverageBrowserCaching

#### CDN(Fastly)における推奨[^3]
- 1度のサイト回遊時間程度に設定する
    - Google Analytics等で分析してもいいが、概ね5分~10分
    - コントロールが効きやすいコンテンツキャッシュ側で長時間のキャッシュをするのよい
        - ETagヘッダーがついていれば、更新していない場合はブラウザキャッシュを再利用する

[^3]: https://www.fastly.com/blog/leveraging-browser-cache-fastlys-cdn

## コンテンツキャッシュ戦略
コンテンツキャッシュを設定する際に考慮するポイントについてまとめてみました。

### 対象
- 静的なコンテンツ
    - プログラム等で動的に生成されていないHTML
    - CSS
    - JavaScript
    - フォントファイル
    - テキストファイル(.txt、.xml、etc...)
    - 画像
    - 動画
    - etc...
- 動的なコンテンツ
    - プログラム等で動的に生成されるHTML
    - JSONデータ
    - etc...

### 条件
- キャッシュはURLおよびVaryヘッダーごとに行われる
    - 同一URLでもVaryヘッダーの内容(Accept-Encoding、User-Agent、etc...)でキャッシュが異なる
- ユーザーごとにコンテンツを出し分けている場合は注意する
    - 個人情報が入っている場合は特に注意
        - 個人情報の流出リスクがある
    - 出し分けしているものはそもそもキャッシュしない
        - Cookieに個人データが多くの場合含まれているため、Set-Cookieがレスポンスについている場合、多くのプロキシ(CDN等)はデフォルトでキャッシュしない
        - URLで明示的に除外しておく
    - 出し分けしている部分を非同期(JavaScript等)で取得するように改修する
        - キャッシュされない部分で表示を変更するようにする
    - 条件が指定できる場合はちゃんと指定する
        - デバイスごとの表示(User-Agent等)
    - アプリケーション(オリジン)からのキャッシュのコントロールにはCache-Controlヘッダーを用いる
    - プロキシ層ではそれぞれの機能を用いてキャッシュを行う
        - Ex: NGINXのキャッシュ、CDNのキャッシュ
        - Cache-Control、Surrogate-Controlに準じてキャッシュさせることも可能
- ETagヘッダーを用いてコンテンツの変更をブラウザに認識させる
    - レスポンスにトークンとしてETagヘッダーを含めておくと以降のアクセスはこのトークンが変わっているかどうかを確認する
    - コンテンツキャッシュが変わっていれば取得、変わらなければサーバーは304 Not Modifiedを返却し、ブラウザが取得をしないでブラウザキャッシュを再利用して表示する
        - データの転送量が減る

### 期間
- コンテンツの変更タイミングに応じて検討する
    - コンテンツの変更時間が決まっている場合はそれに合わせて
    - URLが固定できる場合は長めに
    - Cache-Controlは短め(5分~10分)、Surrogate-Controlは長く設定する
- キャッシュパージの仕組みを用意しておく
    - いざというときにすぐに消せるように
    - パージの仕組みがあればキャッシュ時間を長く取れる
        - 有名CDNでは標準で機能を提供してくれている
        - リリースのタイミングで全削除等すると良い

## Cache-Controlヘッダーについて
キャッシュを導入するにあたり最も重要なCache-Controlヘッダーについてよく使うものをまとめてみました。
下記をアプリケーション側やプロキシで設定することで柔軟にキャッシュをコントロールできるようになります。

### キャッシュ利用設定
- `public`
    - レスポンスは任意のキャッシュ(ブラウザ・プロキシ)によってキャッシュされる
- `private`
    - レスポンスは単一ユーザー向けであり、プロキシ等の共有キャッシュには保存しない
    - ブラウザキャッシュ等のプライベートキャッシュにはレスポンスを保持することができる
- `no-cache`
    - キャッシュを利用せず、オリジンへ取得しに行くことを強制する

### 有効期限
- `max-age=<seconds>`
    - リソースが最新であるとみなされる最大の時間を設定する

### その他
- `must-revalidate`
    - 有効期限切れリソースのキャッシュを利用する前に必ず再検証を行う必要がある
    - また有効期限切れのリソースには使用しない
- `no-store`
    - キャッシュしない

## まとめ
「キャッシュすれば早くなる」、「早くしたいからキャッシュしよう」という話はよく出ますが、実際にどんなことを考慮する必要があるのかまとめてみると膨大かつ複雑で大変でした。
ブラウザ -> プロキシ(CDN、Webサーバー) -> アプリケーションの間でどこでどの設定が利用されて、どこで設定すればいいのかをしっかり整理する必要があるなと感じました。
しかしながら、上手くコントロールできれば確実に高速化が期待でき、UX・SEOに有効になるので今後もしっかり考えていきたいと思いました。