HTTP キャッシュ

ネットワーク経由で情報を取得するには時間もコストもかかります。レスポンスが大きいと、クライアントとサーバー間のラウンドトリップを何度も繰り返す必要があるため、レスポンスが利用可能となってブラウザで処理できるようになるまで時間がかかります。さらに、ユーザー側ではデータの通信コストが発生します。そのため、前に取得したリソースをキャッシュに保存して再使用できることは、パフォーマンスを最適化する上で非常に重要です。

幸い、どのブラウザにも HTTP キャッシュが実装されています。したがって、各サーバーのレスポンスに正しい HTTP ヘッダー ディレクティブがあり、ブラウザがレスポンスをキャッシュに格納できるタイミングとその期間をブラウザに指示していることを確認するだけで済みます。

注: アプリケーションで WebView を使用してウェブ コンテンツを取得および表示している場合は、構成フラグを追加して、HTTP キャッシュの有効化、用途に合った適切なキャッシュ サイズ値の設定、キャッシュの永続化を確実に行う必要があります。設定についてはプラットフォームのドキュメントを参照しください。

HTTP リクエスト

サーバーではレスポンスを返すときに、コンテンツ タイプ、長さ、キャッシュ ディレクティブ、検証トークンなどを記述した一連の HTTP ヘッダーも送っています。上記の Exchange の例では、サーバーは 1024 バイトのレスポンスを返し、クライアントにそのレスポンスを最大 120 秒間キャッシュに格納するよう指示して、検証トークン("x234dff")を提供します。この検証トークンは、レスポンスの有効期限が切れた後で、リソースに変更があったかどうかを確認するために使用できます。

ETag によるキャッシュされたレスポンスの検証

TL;DR

  • サーバーから ETag HTTP ヘッダーを介して検証トークンが渡される。
  • 検証トークンを使うことで効率的なリソース更新チェックが可能となる。リソースに変更がなければデータ転送は発生しない。

初回の取得から 120 秒が経過し、ブラウザが同じリソースに対する新しいリクエストを開始したとしましょう。まず、ブラウザはローカル キャッシュを調べて前回のレスポンスを見つけます。残念ながら、レスポンスは有効期限切れのため使用できません。この時点で、ブラウザは新しいリクエストを発行して完全なレスポンスを新たに取得することもできますが、この処理は非効率的です。リソースに変更がなければ、キャッシュ内のデータとまったく同じものをダウンロードする必要はありません。

この問題は、検証トークンを ETag ヘッダーに指定することで解決できます。サーバーは任意のトークンを生成して返します。通常、このトークンはファイルのコンテンツのハッシュやその他のフィンガープリントです。クライアントはフィンガープリントがどう生成されるかを認識する必要はありません。必要なのは次回のリクエストでそのフィンガープリントをサーバーに送信することだけです。フィンガープリントが変わらなければリソースに変更はないので、ダウンロードを省略できます。

HTTP Cache-Control の例

上記の例では、クライアントは "If-None-Match" HTTP リクエスト ヘッダー内で ETag トークンを自動的に提供し、サーバーはそのトークンを現在のリソースと突き合わせて確認します。リソースに変更がなければ、"304 Not Modified" レスポンスを返し、キャッシュ内のレスポンスに変更がなく、さらに 120 秒後に更新を延期できることをブラウザに通知します。レスポンスを再度ダウンロードする必要がないため、時間と帯域幅を節約できます。

ウェブ デベロッパーとして、この高効率な再検証を利用するにはどうしたらよいでしょうか。実はブラウザがすべての作業を実行してくれます。ブラウザは検証トークンが前に指定されたかどうかを自動的に検出し、発信リクエストにこのトークンを付加し、サーバーから受信したレスポンスに基づいて必要に応じてキャッシュのタイムスタンプを更新します。あとは、サーバーが実際に、必要な ETag トークンを提供していることを確認するだけです。必要な構成フラグについては、サーバーの資料をご確認ください。

注: ヒント: HTML5 Boilerplate プロジェクトには、人気の高いすべてのサーバー向けのサンプル構成ファイルが用意されており、構成フラグと設定ごとに詳細なコメントが記載されています。リストでご希望のサーバーを見つけ、該当する設定を探してコピーするか、推奨される設定でサーバーが設定されていることを確認してください。

Cache-Control

TL;DR

  • 各リソースでは Cache-Control HTTP ヘッダーによってキャッシュ ポリシーを規定可能です。
  • Cache-Control ディレクティブで、レスポンスのキャッシュを許可するユーザー、キャッシュを実施する条件、キャッシュに保存する期間を制御します。

パフォーマンスの最適化の観点で、最良のリクエストとは、サーバーへの通信を必要としないリクエストです。レスポンスのローカルコピーがあれば、ネットワークによる待ち時間を完全に排除でき、データ転送のためにデータを読み込む必要もありません。これを実現するため、HTTP 仕様では、ブラウザやその他の中間キャッシュに各レスポンスをキャッシュできる条件やキャッシュ期間を制御する Cache-Control ディレクティブをサーバーで返すことができます。

注: Cache-Control ヘッダーは HTTP/1.1 仕様の一部として定義されたもので、レスポンス キャッシュ ポリシーの設定に使用されていた以前のヘッダー(Expires など)よりも優先されます。最新のブラウザはすべて Cache-Control に対応しているので、必要なのは指定することだけです。

HTTP Cache-Control の例

「no-cache」と「no-store」

「no-cache」は、同じ URL に対する後続のリクエストへのレスポンスとして、以前返されたレスポンスを使用するには、まずサーバーに問い合わせてレスポンスに変更があったかどうかを確認する必要があることを示します。そのため、適切な検証トークン(ETag)がある場合、no-cache を指定してもキャッシュされたレスポンスを検証するためのラウンドトリップは発生しますが、レスポンスに変更がなければダウンロードを省略できます。

一方、「no-store」はもっと単純です。返されたレスポンスのバージョンにかかわらず、ブラウザのキャッシュやすべての中間キャッシュはそのレスポンスを一切格納できません。たとえば、個人の機密データや銀行データが含まれているレスポンスなどです。ユーザーがこのアセットをリクエストするたびに、リクエストがサーバーに送信され、完全なレスポンスが毎回ダウンロードされます。

「public」と「private」

レスポンスが「public」とマークされている場合は、レスポンスに HTTP 認証が関連付けられているとしても、さらにレスポンスのステータス コードが通常キャッシュ可能になっていない場合でも、レスポンスをキャッシュに保存できます。通常は、明示的なキャッシュ情報(「max-age」など)によってレスポンスがキャッシュ可能であることが指定されているため「public」は必要ありません。

一方、"private" レスポンスは、ブラウザのキャッシュには格納できますが、通常、対象ユーザーは 1 人のため、中間キャッシュに格納することは認められません。たとえば、個人的なユーザー情報を含む HTML ページはそのユーザーのブラウザでのみキャッシュに格納でき、CDN では格納できません。

「max-age」

このディレクティブでは、取得したレスポンスを再使用できる最大時間を、リクエストの時刻を起点とする秒数で指定します。たとえば、"max-age=60" は、レスポンスを 60 秒間キャッシュに格納して再使用できることを示します。

最適な Cache-Control ポリシーの定義

キャッシュ決定木

上記の決定木に従って、アプリケーションで使用する特定のリソース、または一連のリソースに最適なキャッシュ ポリシーを判別します。理想的には、できるだけ多くのレスポンスを、できるだけ長い期間、クライアント上でキャッシュに保存し、レスポンスごとに検証トークンを提供して、効率的な再検証を実現することを目指しましょう。

Cache-Control ディレクティブ & 説明
max-age=86400 レスポンスは、最大 1 日(60 秒 x 60 分 x 24 時間)、ブラウザおよび任意の中間キャッシュでキャッシュに保存可能(つまり、「public」)
private, max-age=600 レスポンスは、クライアントのブラウザで最大 10 分(60 秒 x 10 分)のみキャッシュに保存可能
no-store レスポンスはキャッシュに保存することが許可されず、リクエストごとに完全に取得する必要がある。

HTTP Archive によると、上位 300,000 件のサイト(Alexa のランクによる)の集計では、ダウンロードした全レスポンスの半数近くをブラウザのキャッシュに保存できます。ページビューやアクセスを繰り返す場合、このことは大幅な削減につながります。もちろん、これは、特定のアプリケーションでキャッシュに格納できるのはリソースの 50% である、という意味ではありません。リソースの 90% 以上をキャッシュに格納できるサイトもあれば、キャッシュに一切格納できない個人的なデータや時間依存のデータが多いサイトもあります。

ページを調査してキャシュに保存できるリソースを特定し、このリソースが適切な Cache-Control ヘッダーと ETag ヘッダーを返していることを確認してください。

キャッシュされたレスポンスの無効化と更新

TL;DR

  • ローカル キャッシュされたレスポンスはリソースの「有効期限」まで使用されます。
  • ファイルのコンテンツのフィンガープリントを URL に埋め込むことで、新しいバージョンのレスポンスに更新するようクライアントに強制することが可能です。
  • パフォーマンスを最適化するには各アプリケーションで独自のキャッシュ階層を規定することが必要です。

ブラウザによるすべての HTTP リクエストは、まずブラウザのキャッシュにルーティングされ、リクエストを満たす有効なキャッシュ済みレスポンスがあるかどうかが確認されます。一致するレスポンスがあると、そのレスポンスがキャッシュから読み取られるため、転送によるネットワーク遅延も、データの通信コストも発生しません。

では、キャッシュされたレスポンスを更新するか無効にする必要がある場合はどうすればよいのでしょうか。たとえば、CSS スタイルシートを最大 24 時間(max-age=86400)キャッシュに保存するよう指定してあったときに、デザイナーがすべてのユーザーに提供する必要のある更新をコミットしたとしましょう。どれが CSS の「古い」キャッシュ コピーであるかをユーザーに伝え、ユーザーにキャッシュを更新してもらうにはどうするのでしょうか。このためには、少なくともリソースの URL を変更する必要があります。

レスポンスをブラウザがキャッシュに格納すると、そのキャッシュ バージョンは、max-age や expires で指定されたとおりに有効期限が切れるまで、または他の何らかの理由でキャッシュから削除される(たとえば、ユーザーがブラウザのキャッシュを消去する)まで使用されます。この結果、ページが作成された時点での異なるバージョンのファイルをそれぞれのユーザーが使用している状態が発生することがあります。リソースを取得したばかりのユーザーは新しいバージョンを使用することになる一方で、以前の(ただしまだ有効な)コピーをキャッシュに保存してあるユーザーは古いバージョンのレスポンスを使用することになります。

では、クライアント側のキャッシュと迅速な更新の両方の長所を活かすにはどうすればよいでしょうか。リソースの URL を変更して、コンテンツが変わるたびにユーザーが新しいレスポンスをダウンロードしなければならないようにすればよいのです。通常、この処理はファイルのフィンガープリント、またはバージョン番号をファイル名に埋め込む(たとえば、style.x234dff.css など)で実現されます。

キャッシュ階層

リソース単位のキャッシュ ポリシー機能によって「キャッシュ階層」を定義でき、各リソースをキャッシュに保存する期間だけでなく、訪問者に新しいバージョンが表示されるまでの期間も制御できます。たとえば、上記の例を分析してみましょう。

  • HTML は「no-cache」とマークされています。このため、ブラウザはリクエストごとに常にドキュメントを再検証し、コンテンツに変更があれば最新のバージョンを取得します。また、HTML マークアップ内には CSS アセットと JavaScript アセットの URL にフィンガープリントも埋め込んであります。これらのファイルのコンテンツに変更があると、ページの HTML も変わり、HTML レスポンスの新しいコピーがダウンロードされることになります。
  • この CSS はブラウザと中間キャッシュ(たとえば、CDN)によってキャッシュに保存でき、1 年で期限切れになるように設定されています。1 年という「ずっと先の有効期限」を使用しても問題ないのは、ファイルのフィンガープリントをファイル名に埋め込んであるためです。CSS が更新されると、URL も変更されます。
  • JavaScript の有効期限も 1 年に設定されていますが、private とマークされています。おそらく、これは CDN でキャッシュに保存してはいけないプライベート ユーザー データが含まれているためです。
  • 画像はバージョンも一意のフィンガープリントもなしでキャッシュに保存され、有効期限は 1 日に設定されています。

ETag、Cache-Control、一意の URL を組み合わせることで、長い有効期限、レスポンスのキャッシュを保存できる場所の制御、オンデマンド更新のすべての長所を活かすことができます。

キャッシュのチェックリスト

最善のキャッシュ ポリシーは 1 つではありません。トラフィックのパターン、提供するデータの種類、データ鮮度に関するアプリケーション固有の要件に応じて、リソースごとの適切な設定を規定および設定する必要があり、全体的な「キャッシュ階層」も構成する必要があります。

次に、キャッシュ戦略に取り組むうえで押さえておきたい、おすすめの方法やテクニックを紹介します。

  • 一貫した URL を使用する: 同じコンテンツを異なる URL で提供すると、そのコンテンツは何度も取得および格納されます。注: URL は大文字と小文字が区別されます
  • サーバーが検証トークン(ETag)を提供していることを確認する: サーバーでリソースに変更がない場合、検証トークンがあれば同じデータを転送する必要がなくなります。
  • 中間でキャッシュに格納できるリソースを指定する: すべてのユーザー間でレスポンスが変わらないリソースが、CDN などの中間でのキャッシュ候補です。
  • リソースごとに最適なキャッシュ有効期限を決定する: リソースによって更新要件は異なります。リソースごとに適切な max-age を監査して決定します。
  • サイトに最適なキャッシュ階層を決定する: コンテンツのフィンガープリントを埋め込んだリソースの URL を使用し、さらに HTML ドキュメントの有効期限を短縮するか no-cache を指定することで、クライアントが更新データを取得する間隔を制御できます。
  • 離脱率を最小限に抑える: リソースによって更新頻度は異なります。JavaScript 関数、一連の CSS スタイルなど、リソースの特定の部分が頻繁に更新される場合は、そのコードを別のファイルとして提供することを検討してください。それにより、コンテンツの残りの部分(変更頻度が低いライブラリ コードなど)はキャッシュから取得されて、更新を取得する際にダウンロードするコンテンツの量を最小限に抑えることができます。