ナビゲーションをスキップ
部 IV 章 20

キャッシング

序章

キャッシングとは、一度ダウンロードしたコンテンツを再利用するための技術です。これは、何か(ウェブページを構築するサーバー、CDNなどのプロキシ、またはブラウザ自体)が「コンテンツ」(ウェブページ、CSS、JS、画像、フォントなど)を保存し適切なタグを付けることで再利用できるようにするものです。

非常にハイレベルな例を挙げてみましょう。

Janeは,ウェブサイト www.example.com のホームページを訪れました.Janeはカリフォルニア州ロサンゼルスに住んでおり、example.comのサーバーはマサチューセッツ州ボストンにあります。Janeが www.example.com にアクセスする際には、国を越えて移動しなければならないネットワークリクエストが発生します。

example.comのサーバー(通称:Originサーバー)では、ホームページが取得されます。サーバーはJaneがLAに住んでいることを知り、Janeの近くで開催されるイベントのリストなどの動的コンテンツをページに追加します。その後、ページはアメリカのジェーンに送信され、ジェーンのブラウザに表示されます。

キャッシングがない場合、LAのCarlosがJaneの後に www.example.com を訪れた場合、彼のリクエストは国を越えてexample.comのサーバーに移動しなければなりません。サーバーは、LAのイベントリストを含む同じページを構築しなければなりません。そして、そのページをCarlosに送り返さなければなりません。

さらに悪いことに、Janeがexample.comのホームページを再訪すると、それ以降のリクエストは最初のリクエストと同じようになります。リクエストは国を越えて移動し、example.comのサーバーはホームページを再構築して彼女に送り返さなければなりません。

つまり、キャッシングがなければ、example.comのサーバーは各リクエストを最初から構築することになります。これはサーバーにとって、より多くの仕事をすることになるので悪いことです。さらに、JaneまたはCarlosとexample.comサーバーとの間の通信で、データは国を越えて移動する必要があります。このようなことが重なると、体感速度は低下し、二人にとって良くないことになります。

しかしサーバーキャッシングでは、Janeが最初のリクエストをしたときに、サーバーはホームページのLA版を構築します。これは、すべてのLAの訪問者が再利用できるようにデータをキャッシュします。ですから、Carlosのリクエストがexample.comのサーバーに届いたとき、サーバーはホームページのLA版がキャッシュにあるかどうかをチェックします。Janeの以前のリクエストの結果、そのページはキャッシュに入っているので、サーバーはキャッシュされたページを返すことで時間を節約します。

さらに重要なことは、ブラウザキャッシュではJaneのブラウザは最初のリクエストでサーバからページを受け取ると、そのページをキャッシュします。今後のexample.comのホームページへのリクエストはすべて、ネットワークリクエストなしに、ブラウザのキャッシュから提供されることになります。example.comのサーバも、Janeのリクエストを処理する必要がないというメリットがあります。

Janeは幸せです。Carlosも幸せです。example.comの皆さんも幸せです。みんな幸せです。

ブラウザのキャッシングは、コストのかかるネットワークリクエストを回避することで、パフォーマンス上の大きなメリットをもたらすことは明らかでしょう(ただし、常にエッジケースが存在します)。また、ウェブサイトのオリジン・インフラストラクチャへのトラフィックを減らすことで、アプリケーションの拡張にも役立ちます。サーバーキャッシングは、基礎となるアプリケーションの負荷を大幅に軽減します。

キャッシングは、エンドユーザーにとっても(Webページを早く手に入れられる)、Webページを提供する企業にとっても(サーバーの負荷を軽減できる)メリットがあります。キャッシングはまさにWin-Winの関係なのです。

ウェブ・アーキテクチャでは、一般的に複数の階層のキャッシングが行われます。キャッシングが行われる場所としては、主に4つの場所、すなわちcaching entitiesがあります。

  1. エンドユーザーのブラウザです。
  2. エンドユーザーのブラウザ上で動作するサービスワーカーのキャッシュです。
  3. コンテンツデリバリーネットワーク(CDN)などのプロキシで、エンドユーザーのブラウザとオリジンサーバーの間に設置される。
  4. オリジン・サーバーそのものです。

本章では、オリジンサーバーやCDNでのキャッシングではなく、主にブラウザ内(1-2)でのキャッシングについて説明します。とはいえ、本章で取り上げるキャッシングに関する具体的なトピックの多くは、ブラウザとサーバー(CDNを使用している場合はそのサーバー)との関係に依存しています。

キャッシングやウェブの仕組みを理解するには、リクエストする側(ブラウザなど)とレスポンスする側(サーバなど)の間のトランザクションで成り立っていることを覚えておくとよいでしょう。各トランザクションは2つの部分で構成されています。

  1. リクエストする側のエンティティからの要求。”私はオブジェクトXが欲しい“となります。
  2. 応答する側のエンティティからのレスポンスです。”これがオブジェクトXです“となります。

キャッシングというと、リクエストした側のオブジェクト(HTMLページや画像など)がキャッシュされることを指します。

下図は、オブジェクト(Webページなど)に対する典型的なリクエスト/レスポンスの流れを示しています。ブラウザとサーバの間にはCDNがあります。ブラウザ→CDN→サーバの流れの各ポイントで、各キャッシングエンティティは、まず自分のキャッシュにオブジェクトがあるかどうかを確認します。見つかった場合は、キャッシュされたオブジェクトをリクエスト元に返し、その後、リクエストを次のキャッシュエンティティに転送します。

オブジェクトのリクエスト/レスポンスフロー。
オブジェクトの典型的なリクエスト/レスポンスフローにおけるキャッシュの使用法を示すシーケンス図。
図20.1. オブジェクトのリクエスト/レスポンスフロー。

注:本章では、特に明記しない限り、デスクトップの統計も同様であると理解した上で、すべての統計をモバイル用にしています。モバイルとデスクトップの統計が大きく異なる場合は、その旨を明記しています。 本章で使用するレスポンスの多くは、一般的に入手可能なサーバーパッケージを使用するウェブサーバーからのものです。ベストプラクティスを示すことはできても、使用しているソフトウェアパッケージのキャッシュオプションの数が限られている場合には、ベストプラクティスを実現できない可能性があります。

キャッシングの基本原則

ウェブコンテンツのキャッシュには、3つの指針があります。

  • 可能な限りのキャッシュ
  • できる限り長くキャッシュする
  • エンドユーザーにできるだけ近い場所でキャッシュする

可能な限りのキャッシュ

何をキャッシュするかを検討する際には、レスポンスの内容がstaticなのかdynamicなのかを理解することが重要です。

静的コンテンツ

静的コンテンツの例として、画像があります。たとえばcat.jpgというファイルに収められた猫の写真は、誰がリクエストしてもどこにいても通常は同じものです(もちろん別のフォーマットやサイズで配信されることもありますが、通常は別のファイル名で配信されます)。

はい、猫の写真があります。
ルナという猫の写真です。
図20.2. はい、猫の写真があります。

静的コンテンツは、一般的にキャッシュ可能で、多くの場合、長期間にわたって利用されます。コンテンツ(1つ)とリクエスト(多数)の間には、1対多の関係があります。

ダイナミックコンテンツ

ダイナミックコンテンツの例としては、ある地域に特化したイベントのリストがあります。このリストは、リクエストした人の場所に応じて異なります。

動的に生成されたコンテンツは、より微妙で、慎重な検討が必要です。動的コンテンツの中にはキャッシュできるものもありますが、多くの場合、短い期間しかキャッシュできません。近日開催のイベントのリストの例では、おそらく日ごとに変化します。また、ユーザーのブラウザにキャッシュされるものは、サーバーやCDNにキャッシュされるもののサブセットである可能性もあります。とはいえ、一部のダイナミックコンテンツをキャッシュすることは可能です。「ダイナミック」を「キャッシュできない」の別の言葉だと考えるのは正しくありません。

できる限り長くキャッシュする

リソースをキャッシュする時間の長さは、コンテンツの揮発性、つまり変更の可能性や頻度に大きく依存します。たとえば、画像やバージョン管理されたJavaScriptファイルは、非常に長い時間キャッシュされる可能性があります。APIレスポンスやバージョン管理されていないJavaScriptファイルは、ユーザーに最新のレスポンスを確実に提供するために、より短いキャッシュ期間が必要になるかもしれません。一部のコンテンツは1分以下しかキャッシュされないかもしれません。もちろん、まったくキャッシュされるべきではないコンテンツもあります。これについては、後述のキャッシュの機会を特定するで詳しく説明しています。

もうひとつの留意点は、ブラウザにコンテンツのキャッシュをどれだけ長く指示しても、ブラウザはその時点以前にそのコンテンツをキャッシュから削除することがあるということです。たとえば、より頻繁にアクセスされる他のコンテンツのためのスペースを確保するためにそうすることがあります。しかし、ブラウザは指示された時間以上にキャッシュコンテンツを使用することはありません。

できるだけエンドユーザーの近くにキャッシュする

エンドユーザーの近くにコンテンツをキャッシュすることで、遅延をなくしてダウンロード時間を短縮できます。たとえば、あるリソースがユーザーのブラウザにキャッシュされていればリクエストがネットワークに出ることはなく、ユーザーが必要とするときにはいつでもローカルで利用できます。ブラウザのキャッシュにエントリがない訪問者にとっては、CDNはキャッシュされたリソースが次に返される場所となります。ほとんどの場合、オリジンサーバーと比較して、ローカルキャッシュやCDNからリソースを取得する方が速くなります。

いくつかの専門用語

Caching entity: キャッシングを行うハードウェアまたはソフトウェアのこと。本章では、とくに指定のない限り、「ブラウザ」を「キャッシング・エンティティ」の同義語として使用します。

Time-To-Live (TTL): キャッシュオブジェクトのTTLは、そのオブジェクトがキャッシュに保存される期間を定義するもので、通常は秒単位で測定されます。キャッシュされたオブジェクトがTTLに達すると、そのオブジェクトはキャッシュによって「古い」とマークされます。キャッシュにどのように追加されたか(下記のキャッシュヘッダーの詳細を参照)に応じて、そのオブジェクトは直ちにキャッシュから削除されるか、あるいはキャッシュに残っていても「stale」オブジェクトとしてマークされ再利用の前に再検証が必要になることがあります。

Eviction: オブジェクトがTTLに達したとき、あるいはキャッシュが満杯になったとき、実際にキャッシュから削除される自動化されたプロセスのこと。

Revalidation: 古くなったと判定されたキャッシュオブジェクトは、ユーザーに表示する前、サーバーで「再検証」する必要があります。ブラウザはまず、ブラウザのキャッシュにあるオブジェクトが最新で有効であることをサーバに確認しなければなりません。

ブラウザキャッシングの概要

ブラウザがコンテンツ(Webページなど)を要求すると、コンテンツそのもの(HTMLマークアップ)だけでなく、コンテンツを説明する多くのHTTPレスポンスヘッダー(キャッシュ可能性に関する情報を含む)を含むレスポンスを受け取ります。

キャッシング関連のヘッダーがあるかないかで、ブラウザに3つの重要な情報が伝わります。

  1. キャッシュ性: このコンテンツはキャッシュ可能ですか?
  2. 新鮮さ: キャッシュ可能な場合、どのくらいの期間キャッシュできるのか?
  3. バリデーション: もしキャッシュ可能であれば、その後どのようにしてキャッシュされたバージョンが新鮮であることを確認するのでしょうか?

鮮度を指定するために一般的に使用される2つのHTTPレスポンスヘッダーは、Cache-ControlExpiresです。

  • Expiresは、明示的な有効期限の日時を指定します(つまり、コンテンツの有効期限がいつ切れるかを指定します)。
  • Cache-Controlでは、キャッシュ期間(リクエストされた時からどのくらいの期間、ブラウザにコンテンツをキャッシュできるか)を指定します。

しばしば、これらのヘッダーの両方を指定することがありますが、その場合はCache-Controlが優先されます。

これらのキャッシングヘッダーの完全な仕様は、RFC 7234にあり、4.2 (Freshness)4.3 (Validation)のセクションで説明されていますが、以下ではより詳細に説明します。

Cache-ControlExpires

初期のHTTP/1.0時代のウェブでは、Expiresヘッダーが唯一のキャッシュ関連のレスポンスヘッダーでした。上述したように、このヘッダーは、レスポンスが古くなったとみなされる正確な日時を示すために使用されます。その値は、次のような日時です。

Expires: Thu, 01 Dec 1994 16:00:00 GMT

Expiresヘッダーは、鈍器のようなものと考えてよいでしょう。相対的なキャッシュTTLが必要な場合は、現在の日付/時刻に基づいて適切な値を生成するために、サーバー上で処理を行う必要があります。

HTTP/1.1では、一般的に使用されているすべてのブラウザで長い間サポートされているCache-Controlヘッダーが導入されました。Cache-Controlヘッダーは、キャッシングディレクティブを介して、Expiresよりもはるかに高い拡張性と柔軟性を備えており、複数のディレクティブを一緒に指定できます。さまざまなディレクティブの詳細は以下の通りです。

> GET /static/js/main.js HTTP/2
> Host: www.example.org
> Accept: */*
< HTTP/2 200
< Date: Thu, 23 Jul 2020 03:04:17 GMT
< Expires: Thu, 23 Jul 2020 03:14:17 GMT
< Cache-Control: public, max-age=600

上の簡単な例では、JavaScriptファイルのリクエストとレスポンスを示していますが、わかりやすくするためにいくつかのヘッダーを削除しています。Dateヘッダーは、現在の日付(具体的には、コンテンツが提供された日付)を示しています。Expiresヘッダーは、10分間キャッシュできることを示しています(ExpiresDateヘッダーの違い)。Cache-Controlヘッダーは、max-ageディレクティブを指定しており、これはリソースが600秒(5分)の間キャッシュできることを示している。Cache-ControlExpiresよりも優先されるので、ブラウザはレスポンスを5分間キャッシュし、それ以降は陳腐化したものとしてマークされます。

RFC 7234では、レスポンスにキャッシュ用のヘッダーが存在しない場合、ブラウザはレスポンスをヒューリスティックにキャッシュできます。これは、Last-Modifiedヘッダー(渡された場合)からの時間の10%をキャッシュ期間として提案しています。このような場合ほとんどのブラウザはこの提案のバリエーションを実装していますが、中にはレスポンスを無期限にキャッシュするものや、まったくキャッシュしないものもあります。このようにブラウザによってばらつきがあるため、コンテンツのキャッシュ可能性を確実にコントロールするために、特定のキャッシュルールを明示的に設定することが重要です。

Cache-ControlExpires の使用状況を示す棒グラフです。デスクトップでは、73.6%のレスポンスがCache-Controlヘッダーで提供されています。55.5%がExpiresヘッダーで配信され、54.8%がCache-ControlヘッダーとExpiresヘッダーの両方を使用し、25.6%がどちらのヘッダーも含まれていませんでした。モバイルでは73.5%のレスポンスにCache-Controlヘッダーが付与され、56.2%にExpiresヘッダーが付与され、55.4%にCache-ControlExpiresの両方が付与され、25.6%にCache-ControlExpiresの両方が付与されていないことがわかりました。
図20.3. Cache-ControlおよびExpiresヘッダーの使用法。

モバイルのレスポンスのうち73.5%はCache-Controlヘッダーが付いており、56.2%はExpiresヘッダーが付いていますが、レスポンスに両方のヘッダーが含まれているため、これらのほぼすべて(55.4%)は使用されません。25.6%のレスポンスはどちらのヘッダーも含まれていないため、ヒューリスティックキャッシングの対象となります。

この統計は、昨年のデータと比較すると興味深いものです。

Cache-ControlExpiresの使用状況を示す棒グラフです。デスクトップでは、72.3%のレスポンスがCache-Controlヘッダーで提供されています。56.3%がExpiresヘッダーを付けており、55.2%がCache-ControlExpiresの両方を使用しており、26.7%がどちらのヘッダーも付けていない。モバイルでは71.7%のレスポンスにCache-Controlヘッダーが付与され、56.4%にExpiresヘッダーが付与され、55.5%にCache-ControlExpiresの両方が付与され、27.4%にCache-ControlExpiresの両方が付与されていないことがわかりました。
図20.4. 2019年のCache-ControlExpiresヘッダーの使用法。

また、デスクトップでは、Cache-Controlヘッダーの使用がわずかに増加(1.8%)していますが、古いExpiresヘッダーの使用はわずかに減少(0.2%)しています。デスクトップでは、Cache-Controlがわずかに増加し(1.3%)、Expiresの増加は少ない(0.8%)ことがわかります。つまり、デスクトップのサイトでは、ExpiresではなくCache-Controlヘッダーを追加するケースが増えているようです。

また、Cache-Controlヘッダーで許可されているさまざまなディレクティブを掘り下げていくと、その柔軟性とパワーが、多くのケースでより適していることがわかるでしょう。

Cache-Controlディレクティブ

Cache-Controlヘッダーを使用する際には、特定のキャッシュ機能を示す1つ以上のディレクティブー定義済みの値を指定します。複数のディレクティブはカンマで区切られ、どのような順番でも指定できますが、中にはお互いにぶつかり合うものもあります(例:publicprivate)。いくつかのディレクティブはmax-ageのように値を取るものもあります。

以下は、もっとも一般的なCache-Controlディレクティブの一覧です。

ディレクティブ 説明
max-age 現在の時刻を基準にして、リソースをキャッシュできる秒数を示します。 たとえばmax-age=86400
public ブラウザや、サーバとブラウザの間にあるCDNなどのプロキシを含む、あらゆるキャッシュがレスポンスを保存する可能性があります。これはデフォルトで想定されています。
no-cache キャッシュされたエントリは、たとえstaleとマークされていなくても、使用前に条件付きリクエストで再検証されなければなりません。
must-revalidate 古くなったキャッシュエントリは、使用前に条件付きリクエストで再検証する必要があります。
no-store 応答をキャッシュしてはいけないことを示す。
private レスポンスは特定のユーザーを対象としており、プロキシやCDNなどの共有キャッシュには保存されないようになっています。
proxy-revalidate must-revalidateと同じですが、共有キャッシュに適用されます。
s-maxage max-ageと同じですが、共有キャッシュ(例:CDN)にのみ適用されます。
immutable TTLの間、キャッシュされたエントリが変更されることはなく、再検証は必要ないことを示す。
stale-while-revalidate クライアントが、古い応答を受け入れる一方で、新しい応答をバックグラウンドで非同期的にチェックすることを示します。
stale-if-error 新鮮な応答のチェックに失敗した場合に、クライアントが古い応答を受け入れることを示す。
図20.5. Cache-Controlディレクティブ。

max-ageディレクティブは、Expiresヘッダーと同じように、TTLを直接定義するので、もっともよく見られるものです。

以下は、複数のディレクティブを持つ有効なCache-Controlヘッダーの例です。

Cache-Control: public, max-age=86400, must-revalidate

これはオブジェクトが86,400秒(1日)キャッシュできることを示しており、サーバとブラウザの間のすべてのキャッシュや、ブラウザ自体にも保存できることを意味しています。TTLに達しstaleとマークされたオブジェクトは、キャッシュに残りますが、再利用する前に条件付きで再検証する必要があります。

11個の Cache-Control ディレクティブの分布を示す棒グラフです。デスクトップでの使用率は、max-ageが60.2%、publicが29.7%、no-cacheが14.3%、must-revalidateが12.1%、no-storeが9.2%、privateが9.1%、immutableが3.5%、no-transformが2.3%、stale-while-revalidateが2.1%となっています。 immutableは3.5%、no-transformは2.3%、stale-while-revalidateは2.1%、s-maxageは1.5%、proxy-revalidateは1.0%、stale-if-errorは0.2%となってます。モバイルでは、max-ageが59.7%、publicが29.7%、no-cacheが15.1%、must-revalidateが12.5%、no-storeが9.6%、privateが9.7%、immutableが3.5%となってます。 immutableは3.5%、no-transformは2.2%、stale-while-revalidateは2.2%、s-maxageは1.2%、proxy-revalidateは1.1%、stale-if-errorは0.2%です。
図20.6. Cache-Controlディレクティブの配布。

上の図は、モバイルとデスクトップのウェブサイトで使用されている11個のCache-Controlディレクティブを示しています。これらのキャッシュディレクティブの人気度について、いくつかの興味深い見解があります。

  • モバイルのCache-Controlヘッダーのうち、max-ageは約59.66%、no-storeは約9.64%使用されています(no-storeディレクティブの意味と使用方法については後述します)。
  • publicを明示的に指定する必要はありません。なぜなら、privateが指定されていない限り、キャッシュエントリはpublicとみなされるからです。それにもかかわらず、ほぼ3分の1のレスポンスにはpublicが含まれていて、すべてのレスポンスで数個のヘッダーバイトがムダになっています :)
  • immutableディレクティブは、2017年に導入された比較的新しいもので、FirefoxとSafariでしかサポートされていません。使用率はまだ3.47%程度ですが、Facebook、Google、Wix、Shopifyなどのレスポンスで広く見られます。特定のタイプのリクエストのキャッシュ性を大幅に向上させる可能性があります。

ロングテールに向かうと、ごく一部の無効なディレクティブが見つかります。これらはブラウザでは無視され、ヘッダーバイトをムダにするだけです。これらは大まかに2つのカテゴリーに分けられます。

  • nocaches-max-ageなどのスペルミスや、=の代わりに:を使用したり、-の代わりに_を使用したりするなど、無効なディレクティブの構文があります。
  • max-stale, proxy-public, surrogate-controlのような存在しないディレクティブ。

ムダなディレクティブのリストの中でもっとも興味深いのは、no-cache="set-cookie"の使用です。Cache-Controlヘッダーの全値のわずか0.2%であっても、他の無効なディレクティブの合計よりも多くを占めています。初期のCache-Controlヘッダーに関する議論の中で、この構文は、Set-Cookieレスポンスヘッダー(ユーザー固有のものである可能性)が、CDNなどの中間プロキシによってオブジェクト自体と一緒にキャッシュされないようするための可能な方法として提起されました。しかし、この構文は最終的なRFCには含まれていませんでした。ほぼ同等の機能はprivateディレクティブで実装できますし、no-cacheディレクティブでは値を指定することはできません。

Cache-Control: no-store, no-cachemax-age=0

レスポンスを絶対にキャッシュしてはいけない場合には、Cache-Control: no-storeディレクティブを使用します。このディレクティブが指定されていない場合には、レスポンスは キャッシュ可能であるとみなされ、キャッシュできますno-storeが指定されている場合は、他のディレクティブよりも優先されることに注意してください。これは、キャッシュされるべきではないリソースがキャッシュされた場合に深刻なプライバシーやセキュリティの問題が、発生する可能性があるためです。

レスポンスをキャッシュできないように設定しようとしたときに発生する、いくつかの一般的なエラーを見ることができます。

  • Cache-Control: no-cacheを指定すると、リソースをキャッシュしないように指示しているように聞こえるかもしれません。しかし、上述のとおり、no-cacheディレクティブはリソースのキャッシュを許可します。これは単に、使用前にリソースを再検証するようにブラウザに通知するだけで、リソースのキャッシュをまったく行わないこととは異なります。
  • Cache-Control: max-age=0を設定するとTTLが0秒になりますが、これはキャッシュされないこととは違います。max-age=0が指定されると、リソースはキャッシュされますが、staleとマークされるため、ブラウザは即座にその新鮮さを再検証しなければなりません。

機能的には、no-cachemax-age=0は似ていて、どちらもキャッシュされたリソースの再検証を要求します。no-cacheディレクティブは、0よりも大きな値のmax-ageディレクティブと一緒に使うこともできます。この場合、オブジェクトは指定されたTTLの間キャッシュされますが、毎回使用する前に再検証されます。

上記の3つのディレクティブを見ると、no-storeno-cachemax-age=0の3つのディレクティブを組み合わせたものが2.7%、no-storeno-cacheの両方を組み合わせたものが6.7%、no-storeのみを組み合わせたものは0.15%以下とごくわずかしかありません。

上述したように、no-storeno-cachemax-age=0のどちらかまたは両方と一緒に指定された場合、no-storeディレクティブが優先され、他のディレクティブは無視されます。したがってコンテンツをどこにもキャッシュしたくない場合は、単純にCache-Control: no-storeを指定するだけで十分であり、よりシンプルで最小限のヘッダーバイトしか使用しません。

max-age=0ディレクティブは、no-storeが指定されていないレスポンスの2%未満にしか存在しません。このような場合、リソースはブラウザにキャッシュされますが、すぐにstaleと判定されるため、再検証が必要になります。

条件付きリクエストとリバリデーション

ブラウザが以前にオブジェクトをリクエストしすでにキャッシュに入っているが、そのキャッシュエントリがすでにTTLを超えている(したがってstaleとマークされている)場合や、オブジェクトが使用前に再検証されなければならないものとして定義されている場合などがよくあります。

このような場合、ブラウザはサーバに対して条件付きのリクエストを行うことができます。つまり、「私のキャッシュにはobject Xがあるのですが、これを使ってもいいでしょうか。それとも、もっと新しいバージョンを使った方がいいでしょうか?」。 サーバーは以下の2つの方法で応答します。

  • はい、キャッシュに保存されているバージョンのオブジェクトXは問題なく使用できます。」となります。この場合、サーバーからのレスポンスは、304 Not Modifiedというステータスコードとレスポンスヘッダーで構成され、レスポンスボディはありません。
  • いいえ、ここに最新のバージョンのオブジェクトXがありますので、これを使ってください。” となります。この場合、サーバーからのレスポンスは、200 OKのステータスコード、レスポンスヘッダー、そして新しいレスポンスボディ(実際の新しいバージョンのオブジェクトX)で構成されます。

いずれの場合も、サーバはオプションでキャッシング応答ヘッダーを更新し、オブジェクトのTTLを延長できます。そうすれば、ブラウザは条件付きのリクエストを重ねることなく、さらに長期間オブジェクトを使用できます。

304 Not Modifiedのレスポンスはヘッダーのみで構成されているため、上記はrevalidationと呼ばれ、正しく実装されていれば、知覚的なパフォーマンスを大幅に向上させることができます。それは、200 OKの応答よりもはるかに小さいため、帯域幅が削減され、迅速な応答が可能になります。

では、サーバーはどのようにして条件付きのリクエストと通常のリクエストを識別するのでしょうか?

実際には、オブジェクトへの最初のリクエストに起因するものです。ブラウザがキャッシュにないオブジェクトをリクエストするときは、次のようなGETリクエストを行うだけです。

> GET /index.html HTTP/2
> Host: www.example.org
> Accept: */*

ブラウザが条件付きリクエストを利用できるようにしたい場合(この判断は完全にサーバ側に委ねられています!)、サーバは、オブジェクトが後続の条件付きリクエストの対象であることを識別する2つのレスポンスヘッダーの一方または両方を含めることができます。2つのレスポンスヘッダーとは。

  • Last-Modified: これは、オブジェクトが最後に変更された日時を示します。この値は日付のタイムスタンプです。
  • ETag(エンティティタグ)です。これは、コンテンツのユニークな識別子を引用符付きの文字列で提供します。通常はファイルの内容をハッシュ化したものですが、タイムスタンプや単純な文字列にすることもできます。

両方のヘッダーが存在する場合は、ETagが優先されます。

Last-Modified

ファイルのリクエストを受け取ったサーバーは、次のように、ファイルがもっとも最近変更された日時をレスポンスヘッダーとして含めることができます。

< HTTP/2 200
< Date: Thu, 23 Jul 2020 03:04:17 GMT
< Last-Modified: Mon, 20 Jul 2020 11:43:22 GMT
< Cache-Control: max-age=600

...lots of html here...

ブラウザはこのオブジェクトを(Cache-Controlヘッダーで定義されているように)600秒間キャッシュし、それ以降はオブジェクトが古くなったものとしてマークします。ブラウザがこのファイルを再度使用する必要がある場合は、最初に行ったのと同様にサーバからファイルを要求しますが、今回はIf-Modified-Sinceという追加のリクエストヘッダーを含め最初のレスポンスでLast-Modifiedレスポンスヘッダーに渡された値を設定します。

> GET /index.html HTTP/2
> Host: www.example.org
> Accept: */*
> If-Modified-Since: Mon, 20 Jul 2020 11:43:22 GMT

このリクエストを受け取ったサーバーは、If-Modified-Sinceヘッダーの値と、もっとも最近にファイルを変更した日付を比較することで、オブジェクトが変更されたかどうかを確認できます。

2つの値が同じであれば、サーバはブラウザがファイルの最新バージョンを持っていることを知り、サーバはヘッダー(同じLast-Modifiedヘッダー値を含む)のみでレスポンスボディを持たない304 Not Modifiedレスポンスを返すことができます。

< HTTP/2 304
< Date: Thu, 23 Jul 2020 03:14:17 GMT
< Last-Modified: Mon, 20 Jul 2020 11:43:22 GMT
< Cache-Control: max-age=600

しかし、ブラウザが最後にリクエストしてからサーバ上のファイルが変更された場合、サーバはヘッダー(更新されたLast-Modifiedヘッダーを含む)とボディ内のファイルの新バージョンからなる200 OKレスポンスを返します。

< HTTP/2 200
< Date: Thu, 23 Jul 2020 03:14:17 GMT
< Last-Modified: Thu, 23 Jul 2020 03:12:42 GMT
< Cache-Control: max-age=600

...lots of html here...

ご覧の通り、Last-ModifiedレスポンスヘッダーとIf-Modified-Sinceリクエストヘッダーはペアで動作しています。

エンティティタグ(ETag

この機能は、前述の日付ベースのLast-Modified/If-Modified-Since条件付きリクエスト処理とほぼ同じです。

しかし、この場合、サーバーは日付のタイムスタンプではなく、ETagレスポンスヘッダーを送信します。ETagは単なる文字列で、ファイルの内容をハッシュ化したものや、サーバが算出したバージョン番号などが多いです。この文字列のフォーマットはサーバが自由に決めることができます。唯一の重要な事実は、サーバはファイルを変更するたびにETagの値を変更するということです。

この例では、サーバーがファイルの最初のリクエストを受信したときに、次のようにETag応答ヘッダーでファイルのバージョンを返すことができます。

< HTTP/2 200
< Date: Thu, 23 Jul 2020 03:04:17 GMT
< ETag: "v123.4.01"
< Cache-Control: max-age=600

...lots of html here...

上記のIf-Modified-Sinceの例と同様に、ブラウザはCache-Controlヘッダーで定義されているように、このオブジェクトを600秒間キャッシュします。このオブジェクトを再度サーバーにリクエストする必要がある場合は、If-None-Matchと呼ばれる追加のリクエストヘッダーを含めます。このリクエストヘッダーには、最初のレスポンスのETagレスポンスヘッダーで渡された値が含まれています。

> GET /index.html HTTP/2
> Host: www.example.org
> Accept: */*
> If-None-Match: "v123.4.01"

このリクエストを受け取ったサーバーは、If-None-Matchヘッダー値と、そのファイルの現在のバージョンを比較することで、オブジェクトが変更されたかどうかを確認できます。

2つの値が同じであれば、ブラウザはファイルの最新バージョンを持っていることになり、サーバーはヘッダーだけで304 Not Modifiedレスポンスを返すことができます。

< HTTP/2 304
< Date: Thu, 23 Jul 2020 03:14:17 GMT
< ETag: "v123.4.01"
< Cache-Control: max-age=600

しかし値が異なる場合はサーバー上のファイルのバージョンが、ブラウザが持っているバージョンよりも新しいということになるので、サーバーはヘッダー(更新されたETagヘッダーを含む)とファイルの新しいバージョンからなる200 OKレスポンスを返します。

< HTTP/2 200
< Date: Thu, 23 Jul 2020 03:14:17 GMT
< ETag: "v123.5.06"
< Cache-Control: public, max-age=600

...lots of html here...

ここでも、この条件付きリクエスト処理には、ETagレスポンスヘッダーとIf-None-Matchリクエストヘッダーという2つのヘッダーが使われています。

Cache-ControlヘッダーがExpiresヘッダーよりも強力で柔軟性があるのと同じように、ETagヘッダーは多くの点でLast-Modifiedヘッダーよりも改良されています。これには2つの理由があります。

  1. サーバーは、ETagヘッダーに独自のフォーマットを定義できます。上の例では、バージョンの文字列を示していますが、ハッシュやランダムな文字列にすることもできます。これを許可すると、オブジェクトのバージョンは日付に明示的にリンクされません。そのため、サーバーはファイルの新しいバージョンを作成しても、以前のバージョンと同じETagを与えることができます。

  2. ETagは’strong’または’weak’のいずれかとして定義でき、これによりブラウザは異なる方法で検証できます。この機能を完全に理解し、議論することはこの章の範囲を超えていますが、 RFC 7232に記載されています。

しかし、ETagはサーバの最終更新時刻をベースにしていることが多いので、多くの実装で事実上同じになってしまう可能性があります。さらに悪いことに、サーバの実装(とくにApache)にはさまざまなバグがあり、ETagを使うことがあまり効果的ではないこともあります

棒グラフで見ると、デスクトップではLast-Modifiedが73.5%、ETagが47.9%、両方が42.8%、どちらでもないが21.4%となっています。モバイルの場合もほぼ同じで、Last-Modifiedが72.0%、ETagが46.2%、両方が41.0%、どちらでもないが22.9%となっています。
図20.7. Last-ModifiedETagヘッダーによる新鮮さの検証の採用。
棒グラフで見ると、デスクトップではLast-Modifiedが72.7%、ETagが48.0%、両方が43.1%、どちらでもないが22.4%となっています。モバイルの場合もほぼ同じで、Last-Modifiedが72.0%、ETagが47.1%、両方が42.1%、どちらでもないが23.1%となっています。
図20.8. 2019年には、Last-ModifiedETagヘッダーによる新鮮さの検証を採用。

モバイルのレスポンスの72.0%がLast-Modifiedヘッダー付きで提供されていることがわかります。2019年との比較では、モバイルでの使用率は横ばいですが、デスクトップではわずかに増加しています(1%未満)。

ETagヘッダーを見ると、携帯電話の回答の46.2%がこれを使用しています。これらの回答のうち、34.38%がstring、9.81%がweak、残りの1.98%が無効となっています。Last-Modifiedとは対照的に、ETagヘッダーの使用率は、2019年と比較してわずかに減少しています(1%未満の減少)。

41.0%のモバイルレスポンスは、両方のヘッダーを含んでおり、上述の通りこの場合はETagヘッダーが優先されます。22.9%のモバイルレスポンスは、Last-ModifiedETagのどちらのヘッダーも含まれていません。

条件付きリクエストを使用して再検証を正しく実装すれば帯域幅(304応答は一般的に200応答よりもはるかに小さい)、サーバーの負荷(変更日やハッシュを比較するために必要な処理はわずか)、および知覚的なパフォーマンスの向上(サーバーは304でより迅速に応答する)を大幅に削減できます。しかし上記の統計からもわかるように、全リクエストの5分の1以上は、どのような形式の条件付きリクエストも使用していません。

クロールでは空のキャッシュを使用しており、304レスポンスはほとんどがMethodologyではテストしていない後続の訪問に役立つものなので、これは予想外のことではありません。それでも、304がどのように使用されているかを確認するためにこれらを分析しました。

304 Not Modifiedの分布を示す棒グラフ。デスクトップのレスポンスのうち20.5%は、ETagヘッダーがなく、対応するリクエストのIf-Modified-Sinceヘッダーで渡されたLast-Modifiedの値と同じものを含んでいました。そのうち86%は304 Not Modifiedというステータスでした。レスポンスの86.1%は、対応するリクエストのIf-None-Matchヘッダーで渡される同じETag値を含んでいた。このうち88.9%は304 Not Modifiedとなっています。17.2%のモバイルレスポンスは、ETagヘッダーを持たず、対応するリクエストのIf-Modified-Sinceヘッダーに渡された同じLast-Modified値を含んでいました。そのうち78.3%は304 Not Modifiedというステータスを持っていた。89.9%のレスポンスには、対応するリクエストのIf-None-Matchヘッダーに渡された同じETagの値が含まれていた。そのうち90.2%は304 Not Modifiedとなっています。
図20.9. 304 Not Modifiedステータスの分布

モバイルのレスポンスのうち17.2%(デスクトップ20.5%)は、ETagヘッダーを持たず、対応するリクエストのIf-Modified-Sinceヘッダーで渡されたのと同じLast-Modified値を含んでいることがわかりました。そのうち78.3%(デスクトップでは86%)が304 Not Modifiedというステータスでした。

モバイルのレスポンスの89.9%(デスクトップ86.1%)が、対応するリクエストのIf-None-Matchヘッダーで渡された同じETagの値を含んでいました。また、If-Modified-Sinceヘッダーが存在する場合は、ETagが優先されます。これらのうち、90.2%(デスクトップでは88.9%)が304 Not Modifiedというステータスでした。

日付文字列の妥当性

このドキュメントでは、タイムスタンプを伝えるためのキャッシュ関連のHTTPヘッダーについて説明してきました。

  • Dateレスポンスヘッダーは、リソースがクライアントに提供された日時を示します。
  • Last-Modifiedレスポンスヘッダーは、リソースがサーバー上で最後に変更された日時を示します。
  • Expiresヘッダーは、リソースがどのくらいの期間キャッシュ可能かを示すために使用されます。

これら3つのHTTPヘッダーは、いずれもタイムスタンプを表すために日付形式の文字列を使用しています。日付形式の文字列は、RFC 2616で定義されており、GMTタイムスタンプの文字列を指定する必要があります。 たとえば、以下のようになります。

> GET /index.html HTTP/2
> Host: www.example.org
> Accept: */*

< HTTP/2 200
< Date: Thu, 23 Jul 2020 03:14:17 GMT
< Cache-Control: max-age=600
< Last-Modified: Mon, 20 Jul 2020 11:43:22 GMT

無効な日付文字列はほとんどのブラウザで無視されるため、その文字列が提供されたレスポンスのキャッシュ可能性に影響を与えます。たとえば無効なLast-Modifiedヘッダーは、その無効なタイムスタンプなしにキャッシュされているため、ブラウザはその後、そのオブジェクトに対して条件付きのリクエストを実行できません。

無効な日付の分布を示す棒グラフ。デスクトップでは0.1%、Last-Modifiedで0.5%、Expiresで2.5%の割合で無効なDateが検出されています。モバイルでもほぼ同様で、Dateに0.1%、Last-Modifiedに0.7%、Expiresに2.9%の割合で不正な日付が含まれています。
図20.10. レスポンスヘッダーの日付フォーマットが無効です。

HTTPレスポンスヘッダーのDateは、ほとんどの場合、ウェブサーバによって自動的に生成されるため、無効な値は非常にまれです。同様に、Last-Modifiedヘッダーでも、無効な値の割合は非常に低く(モバイルで0.75%、デスクトップで0.5%)なっています。しかし、非常に意外だったのは、Expiresヘッダーの2.94%が無効な日付フォーマットを使用していたことです(デスクトップでは2.5%)。

Expiresヘッダーの無効な使い方の例としては、以下のようなものがあります。

  • 有効な日付フォーマットですが、GMT以外のタイムゾーンを使用しています。
  • 0や-1などの数値
  • Cache-Controlヘッダーで有効な値

無効なExpiresヘッダーの大きな原因のひとつは、人気のあるサードパーティから提供されたアセットで、日付/時間がESTタイムゾーンを使用しています。Tue, 27 Apr 1971 19:44:06 EST。 ブラウザによっては、堅牢性の観点からこの日付形式を理解して受け入れるものもありますが、そうであると仮定してはいけません。

Varyヘッダー

これまでキャッシュエンティティは、レスポンスオブジェクトがキャッシュ可能か、またどのくらいの期間キャッシュできるかを判断する方法について説明してきました。しかしキャッシュエンティティーが行わなければならない、もっとも重要なステップの1つは、リクエストされたリソースがすでにキャッシュにあるかどうかを判断することです。これは簡単に見えるかもしれませんが、多くの場合URLだけでは判断できません。たとえば同じURLのリクエストでも、使用した圧縮方法(gzip、Brotliなど)が異なっていたり、異なるエンコーディング(XML、JSONなど)で返されたりすることがあります。

この問題を解決するために、キャッシング・エンティティがオブジェクトをキャッシュする際には、そのオブジェクトに一意の識別子(キャッシュ・キー)を与えます。オブジェクトがキャッシュにあるかどうかを判断する必要があるときは、キャッシュキーをルックアップとして使用してオブジェクトの存在を確認します。デフォルトでは、このキャッシュキーはオブジェクトの取得に使用された単純なURLですが、サーバはVaryレスポンスヘッダーを含めることによってキャッシュキーにレスポンスの他の属性(圧縮方法など)を含めるようにキャッシュエンティティに指示できます。Varyヘッダーは、URL以外の要素に基づいて、オブジェクトのバリエーションを識別します。

Varyレスポンスヘッダーは、1つまたは複数のリクエストヘッダーの値をキャッシュキーに追加するようにブラウザに指示します。このVary: Accept-Encodingのもっとも一般的な例、ブラウザは、異なるAccept-Encodingリクエストヘッダー値(例: gzip, br, deflate)に基づいて、同じオブジェクトを異なるフォーマットでブラウザがキャッシュすることになります。

キャッシング用のエンティティは、HTMLファイルのリクエストを送信し、gzip形式のレスポンスを受け入れることを示します。

> GET /index.html HTTP/2
> Host: www.example.org
> Accept-Encoding: gzip

サーバーはオブジェクトで応答し、送信するバージョンがAccept-Encodingリクエストヘッダーの値を含むべきであることを示します。

< HTTP/2 200 OK
< Content-Type: text/html
< Vary: Accept-Encoding

この単純化された例では、キャッシング エンティティは、URLとVaryヘッダーの組み合わせを使用してオブジェクトをキャッシュします。

もうひとつの一般的な値はVary: Accept-Encoding, User-Agentで、これはクライアントに対して、Accept-EncodingUser-Agentの両方の値をキャッシュキーに含めるように指示します。しかし共有プロキシやCDNについて議論する場合、Accept-Encoding以外の値を使用すると、キャッシュが希釈または断片化されキャッシュから提供されるトラフィックの量が、減少する可能性があるため問題となることがあります。たとえば、User-Agentには数千の異なる種類があるため、CDNがあるオブジェクトの多くの異なるバリエーションをキャッシュしようとすると、ほとんど同一の(あるいは実際に同一の)キャッシュされたオブジェクトでキャッシュがいっぱいになってしまう可能性があります。これは非常に非効率的であり、CDN内でのキャッシングが最適でなくなり、キャッシュヒット数の減少とレイテンシーの増大を招くことになります。

一般的に、キャッシュを変更するのは、そのヘッダーに基づいてクライアントに別のコンテンツを提供する場合だけにすべきです。

HTTPレスポンスの43.4%がVaryヘッダーを使用しており、そのうち84.2%がCache-Controlヘッダーを含んでいます。

下のグラフは、Varyヘッダーの上位10個の値の人気度を示しています。User-Agentが10.7%、Origin(CORSの処理に使用)が8%、Acceptが4.1%となっており、Varyの使用量の約92%を占めています。

Varyヘッダーの分布を示す棒グラフ。デスクトップでは91.8%がAccept-Encodingを使用しており、その他はUser-Agentが10.7%、Originが約8.0%、AcceptAccess-Control-Request-HeadersAccess-Control-Request-MethodCookieX-Forwarded-ProtoAccept-LanguageRangeが0.5%~4.1%となっており、かなり小さい値となっています。モバイル応答の91.3%がAccept-Encodingを使用しており、その他の値は非常に小さく、User-Agentが11.0%、Originが約9.1%、AcceptAccess-Control-Request-HeadersAccess-Control-Request-MethodCookieX-Forwarded-ProtoAccept-LanguageRangeが0.6%~3.9%となっています。
図20.11. Varyヘッダーの使用方法。

キャッシュ可能なレスポンスにCookieを設定する

レスポンスがキャッシュされると、そのレスポンス ヘッダーの全セットがキャッシュされたオブジェクトに含まれます。ChromeでキャッシュされたレスポンスをDevToolsで検査すると、レスポンス・ヘッダーが表示されるのはこのためです。

キャッシュされたリソースのためのChrome Dev Toolsです。
Chrome Dev Toolsによると、レスポンスがキャッシュされると、レスポンスヘッダーの全セットがキャッシュされたオブジェクトに含まれます。
図20.12. キャッシュされたリソースのためのChrome Dev Toolsです。

しかし、レスポンスにSet-Cookieがある場合はどうなるでしょうか?RFC 7234 Section 8によると、Set-Cookieレスポンスヘッダーの存在はキャッシュを阻害しないそうです。つまり、キャッシュされたエントリーにSet-Cookieレスポンスヘッダーが含まれている可能性があるということです。このRFCでは、レスポンスのキャッシュ方法を制御するために、適切な Cache-Controlヘッダーを設定することを推奨しています。

私のリクエストに応じてサーバーから私に送信されたSet-Cookieレスポンスヘッダーには、私のCookieが含まれていることが明らかなので、私のブラウザがそれらをキャッシュしても問題はありません。しかし、私とサーバーの間にCDNがある場合、サーバーはCDNに対して、レスポンスをCDN自体にキャッシュすべきでないことを示さなければなりません。そうすれば、私向けのレスポンスがキャッシュされないで、他のユーザーに(私のSet-Cookieヘッダーを含めて)提供されることになります。

たとえば、ログインCookieやセッションCookieがCDNのキャッシュされたオブジェクトに存在する場合、そのCookieは他のクライアントによって再利用される可能性があります。これを避けるための主な方法は、サーバーがCache-Control: privateディレクティブを送信することです。

キャッシュ可能なレスポンスにおけるSet-Cookieの使用状況を示す棒グラフ。キャッシュ可能なデスクトップ向けレスポンスの41.4%、モバイル向けレスポンスの40.4%がSet-Cookieヘッダーを含んでいます。
図20.13. キャッシュ可能なレスポンスのSet-Cookie

キャッシュ可能なモバイルレスポンスのうち、40.4%がSet-Cookieヘッダーを含んでいます。これらのレスポンスのうち、privateディレクティブを使用しているのは4.9%。残りの95.1%(1億9,860万のHTTPレスポンス)は、少なくとも1つのSet-Cookieレスポンスヘッダーを含んでおり、CDNなどのパブリックキャッシュサーバでキャッシュできます。これは、キャッシュ機能とCookieがどのように共存しているのか、理解されていないことを示しているのかもしれません。

キャッシュ可能なprivateと非プライベートのレスポンスにおけるSet-Cookieの使用率を示す棒グラフです。Set-Cookieヘッダーを含むデスクトップレスポンスのうち、4.6%がprivateディレクティブを使用しています。95.4%のレスポンスが、プライベートとパブリックの両方のキャッシュサーバでキャッシュ可能です。Set-Cookieヘッダーを含むモバイルのレスポンスのうち、4.9%がprivateディレクティブを利用しています。95.1%のレスポンスが、プライベートとパブリックの両方のキャッシュサーバでキャッシュされます。
図20.14. privateおよび非プライベートのキャッシュ可能なレスポンスにおけるSet-Cookie

サービス・ワーカー

サービスワーカーとはHTML5の機能の1つで、フロントエンドの開発者がWebページの通常のリクエスト/レスポンスの流れの外で実行されるべきスクリプトを指定し、メッセージを介してWebページと通信することを可能にします。サービスワーカーの一般的な用途としては、バックグラウンドでの同期やプッシュ通知、そして当然ながらキャッシュが挙げられます。

サービスワーカーの管理ページの増加を示す棒グラフ。2019年の0.6%から2020年には1.0%まで採用率が伸びてます
図20.15. サービスワーカーの成長は、2019年からのページを制御しています。

採用率はウェブサイトの1%にとどまっていますが、2019年7月以降、着実に増加しています。Progressive Web Appの章では、上のグラフで1回しかカウントされていない人気サイトでの利用により、このグラフが示す以上に多く利用されていることなど詳しく説明しています。

サービスワーカーを使わないサイト サービスワーカーを使っているサイト 総サイト数
6,225,774 64,373 6,290,147
図20.16. サービス・ワーカーを利用しているウェブサイトの数

上の表では、全6,290,147サイトのうち、64,373サイトがサービスワーカーを導入していることがわかります。

HTTPサイト HTTPSサイト 合計サイト
1,469 62,904 64,373
図20.17. HTTP/HTTPSでサービスワーカーを利用しているウェブサイトの数。

これをHTTPとHTTPSで分けてみると、さらに興味深いことがわかります。HTTPSはサービスワーカーを使用するための必須条件であるにもかかわらず、以下の表によると、サービスワーカーを使用しているサイトのうち1,469件がHTTPで提供されています。

どのようなコンテンツをキャッシングするのか?

これまで見てきたように、キャッシュ可能なリソースは一定期間ブラウザに保存され、その後のリクエストで再利用できるようになります。

キャッシュ可能な回答の割合を示す棒グラフ。デスクトップでは9.2%、モバイルでは9.6%の回答がキャッシュされています。
図20.18. キャッシュ可能なレスポンスとキャッシュ不可能なレスポンスの分布。

すべてのHTTP(S)リクエストにおいて、90.4%のレスポンスがキャッシュ可能であると考えられています(つまり、キャッシュの保存が許可されています)。残りの9.6%のレスポンスは、ブラウザのキャッシュに保存できません。

キャッシュ可能なレスポンスにおけるTTLの分布を示す棒グラフ。デスクトップのレスポンスの4.2%がTTLゼロ、59.4%がゼロより大きいTTL、28.2%がヒューリスティックTTLを使用しています。モバイルのレスポンスの4.2%がTTLゼロ、58.8%がTTLゼロより大きい、28.4%がヒューリスティックTTLを使用しています。
図20.19. キャッシュ可能なレスポンスにおけるTTLの分布。

もう少し掘り下げてみると4.1%のリクエストのTTLが0秒で、この場合オブジェクトはキャッシュに追加されますが、すぐに古いものとしてマークされ再検証が必要になります。28.4%はCache-ControlExpiresなどのヘッダーがないためヒューリスティックにキャッシュされ、58.8%は0秒以上キャッシュされています。

以下の表は、モバイルリクエストのキャッシュTTL値をタイプ別にまとめたものです。

キャッシュのTTLパーセンタイル(単位:時間)
タイプ 10 25 50 75 90
Audio 6 6 240 744 8,760
CSS 24 24 720 8,760 8,760
Font 720 8,760 8,760 8,760 8,760
HTML 0 3 336 8,760 8,600
Image 6 168 720 8,760 8,766
その他 0 3 31 336 23,557
Script 0 4 720 8,760 8,760
Text 0 1 6 24 8,760
Video 6 336 336 336 8,674
XML 1 24 24 24 720
図20.20. リソースタイプ別のモバイルキャッシュTTLパーセンタイル。

ほとんどのTTLの中央値は高いのですが、低いパーセンタイルでは、キャッシュの機会を逃しているものがいくつかあります。たとえば画像のTTLの中央値は720時間(1か月)ですが、25th パーセンタイルはわずか168時間(1週間)で、10th パーセンタイルはわずか数時間にまで落ち込んでいます。これをフォントと比較すると、フォントのTTLは8,760時間(1年)と非常に高く25thパーセンタイルまであり、10thパーセンタイルでも1か月のTTLを示しています。

コンテンツタイプ別のキャッシュ可能性を下の図で詳しく見てみると、フォント、ビデオやオーディオ、CSSファイルは100%近くブラウザキャッシュされている一方で(これらのファイルは一般的に静的であるため、これは理にかなっている)、全HTMLレスポンスの約3分の1はキャッシュ不可能であると考えられます。

キャッシュ可能なリソースの種類を棒グラフで表したもの。デスクトップでは、audioの99.3%、CSSの99.3%、fontの99.8%、HTMLの67.9%、imagesの91.2%、その他のタイプの66.3%、scriptsの95.2%、textの78.6%、videoの99.6%、xmlの81.4%がキャッシュ可能でした。モバイルでは、audioの99.0%、CSSの99.0%、fontの99.8%、HTMLの71.5%、imagesの89.9%、その他のタイプの67.9%、scriptsの95.1%、textの78.4%、videoの99.7%、xmlの80.6%がキャッシュ可能です。
図20.21. コンテンツタイプ別のキャッシュ可能性の分布

また、デスクトップでは、imagesの10.1%、scriptsの4.9%がキャッシュされません。これらのオブジェクトの中には静的なものもあり、より高い割合でキャッシュできる可能性があるため、ここには改善の余地があると思われます。覚えておいてください: できる限り多くのものを、できる限り長くキャッシュしましょう!

キャッシュのTTLはリソースの年齢と比べてどうなのか?

これまでに、サーバーがクライアントにキャッシュ可能なコンテンツをどのように伝え、どのくらいの期間キャッシュされているかについて説明してきました。キャッシュルールを設計する際には、提供しているコンテンツがどれくらい古いかを理解することも重要です。

クライアントへ返信するレスポンスヘッダーに指定するキャッシュTTLを選択する際には、自問してみてください。「どのくらいの頻度でこれらの資産を更新しているか」と「コンテンツの感度はどうか」。たとえば、ヒーロー画像が頻繁に変更されない場合は、非常に長いTTLでキャッシュできます。対照的にJavaScriptファイルが頻繁に変更されるのであれば、ユニークなクエリ文字列でバージョン管理し長いTTLをキャッシュするか、あるいはもっと短いTTLでキャッシュすべきでしょう。

以下のグラフは、コンテンツタイプ別のリソースの相対的な古さを示しています。

コンテンツの年齢を示すスタックバーチャートで、0~52週目、1年以上、2年以上に分けられ、nullやマイナスの数値も表示されます。統計は、ファーストパーティとサードパーティに分かれています。値0は、とくにファーストパーティのHTML、text、xml、およびすべてのアセットタイプのサードパーティリクエストの最大50%に使用されています。中間の年の使用も混在しており、1年目と2年目にはかなりの使用があります。
図20.22. コンテンツタイプ別のリソースエイジ(ファーストパーティ)。
コンテンツの年齢を示すスタックバーチャートで、0~52週目、1年以上、2年以上に分けられ、nullやマイナスの数値も表示されます。統計は、ファーストパーティとサードパーティに分かれています。値0は、とくにファーストパーティのHTML、text、xml、およびすべてのアセットタイプのサードパーティリクエストの最大50%に使用されています。中間の年の使用も混在しており、1年目と2年目にはかなりの使用があります。
図20.23. コンテンツタイプ別のリソースエイジ(サードパーティ)。

このデータの中で興味深い観察結果があります。

  • ファーストパーティのHTMLは、リソース年数がもっとも短いコンテンツタイプで、41.1%のリクエストが1週間未満となっています。その他のほとんどのコンテンツタイプでは、サードパーティコンテンツの方がファーストパーティコンテンツよりもリソースエイジが小さい。
  • ウェブ上のファーストパーティコンテンツのうち、8週間以上経過しているものには、images(78.9%)、scripts(68.7%)、CSS(74.9%)、Webフォント(80.4%)、autio(78.2%)、video(79.3%)など、伝統的にキャッシュ可能なオブジェクトがあります。
  • ファーストパーティとサードパーティのリソースでは、1週間以上経過しているものがあり、大きな差があります。ファーストパーティ製CSSの93.4%が1週間以上経過しているのに対し、サードパーティ製CSSでは48.0%が1週間以上経過している。

リソースのキャッシュ可能性とその年齢を比較することで、TTLが適切か低すぎるかを判断できます。

たとえば、2020年10月18日に配信されたリソースの最終更新日は2020年8月30日であり、配信時には1か月以上経過していることになります。これは頻繁に変更されないオブジェクトであることを示しています。しかし、Cache-Controlヘッダーによると、ブラウザは86,400秒(1日)しかキャッシュできないことになっています。これは、ブラウザが条件付きでも再リクエストする必要がないように、TTLを長くすることが適切なケースです。とくに、ユーザーが数日間に何度も訪れるようなウェブサイトの場合はそうです。

> HTTP/1.1 200
> Date: Sun, 18 Oct 2020 19:36:57 GMT
> Content-Type: text/html; charset=utf-8
> Content-Length: 3052
> Vary: Accept-Encoding
> Last-Modified: Sun, 30 Aug 2020 16:00:30 GMT
> Cache-Control: public, max-age=86400

ウェブ上で提供されているモバイルリソースのうち60.2%が、コンテンツの年齢に比べて短すぎると思われるキャッシュTTLを持っていました。さらに、TTLと年齢の差の中央値は25日で、これもキャッシュ不足が顕著であることを示しています。

クライアント ファーストパーティ サードパーティ 全体
デスクトップ 61.6% 59.3% 60.7%
モバイル 61.8% 57.9% 60.2%
図20.24. TTLが短いリクエストの割合。

上の表でファーストパーティとサードパーティに分けてみると、ファーストパーティのリソースの約3分の2(61.8%)がTTLを長くすることでメリットを得られることがわかります。このことから、キャッシュ可能なものにとくに注意を払い、キャッシュを正しく設定していることを確認する必要があります。

キャッシングの機会を特定する

GoogleのLighthouseツールでは、ウェブページに対する一連の監査を実行でき、cache policy auditでは、サイトに追加のキャッシュが必要かどうかを評価します。これは、コンテンツの年齢(Last-Modifiedヘッダーによる)をキャッシュのTTLと比較し、リソースがキャッシュから提供される確率を推定することによって行われます。スコアに応じて、キャッシュを推奨する結果が表示され、キャッシュ可能な特定のリソースのリストが表示されます。

キャッシュ・ポリシーの改善の可能性を示すライトハウス・レポート。
キャッシュポリシーの改善の可能性を強調したLighthouseレポートの抜粋。ファーストパーティとサードパーティのURL、そのキャッシュTTL、およびサイズが示されている。
図20.25. キャッシュ・ポリシーの改善の可能性を示すライトハウス・レポート。

Lighthouseでは、各監査に対して0%から100%の範囲でスコアを計算し、それらのスコアを総合的なスコアに反映させています。キャッシングのスコアは、潜在的なバイト削減量に基づいています。Lighthouseの結果を見ると、どれだけのサイトがキャッシュポリシーをうまく活用しているかがわかります。

モバイルWebページのuses-long-cache-ttlに関するLighthouse監査スコアの分布を示す棒グラフ。0.10未満が37.5%、0.10~0.39が28.8%、0.40~0.79が17.7%、0.80~0.99が12.1%となっています。1が3.3%、スコア無しが0.6%でした。
図20.26. LighthouseキャッシングのTTLスコアの分布。

100%のスコアを獲得したサイトはわずか3.3%であり、大多数のサイトがキャッシュの最適化によって恩恵を受けることができると考えられます。約3分の2のサイトが40%を下回り、約3分の1のサイトが10%を下回りました。このことから、かなりの量のキャッシュが不足しており、ネットワーク上で過剰なリクエストやバイトが処理されていることがわかります。

また、Lighthouseはより長いキャッシュポリシーを有効にすることで、繰り返し表示する際に何バイト節約できるかを示しています。

モバイルWebページのLighthouseキャッシング監査で得られたバイト削減可能量の分布を示す棒グラフ。回答のうち57.2%が1MB未満、21.58%が1~2MB、7.8%が2~3MB、4.3%が3~4MBのサイズを削減しています。9.2%は4MB以上の節約に成功しています。
図20.27. Lighthouseキャッシング監査による潜在的なバイト削減量の分布。

キャッシュを追加することで恩恵を受けることができるサイトのうち、5分の1以上のサイトが2MB以上もページの重量を減らすことができます。

結論

キャッシングは、ブラウザやプロキシ、CDNなどの仲介者がウェブコンテンツを保存し、エンドユーザーに提供できる非常に強力な機能です。往復の時間を短縮し、コストのかかるネットワークリクエストを最小限に抑えることができるため、パフォーマンス面でのメリットが大きいのです。

キャッシングは非常に複雑なテーマであり、開発サイクルの後半まで放置され、最後の最後で追加されることもよくあります(サイト開発者が設計中に最新バージョンのサイトを見たいという要求)。またキャッシュルールは一度定義されると、サイトの基本的なコンテンツが変更されても、変更されないことが多い。慎重に検討することなく、デフォルト値が選ばれることもよくあります。

オブジェクトを正しくキャッシュするために、キャッシュされたエントリを検証するだけでなく、新鮮さを伝えることができる多くのHTTPレスポンスヘッダーがあり、Cache-Controlディレクティブは非常に大きな柔軟性とコントロールを提供します。

一般的にキャッシュできないと考えられているオブジェクトタイプやコンテンツの多くは、実際にキャッシュできます(覚えておいてください:できるだけ多くのキャッシュをしましょう!)。また、多くのオブジェクトはキャッシュされる期間が短すぎるため、繰り返しリクエストや再検証が必要になります(覚えておいてください:できるだけ多くのキャッシュをしましょう!)。しかしウェブサイトの開発者は、コンテンツを過剰にキャッシュすることで、ミスの機会が増えることに注意しなければなりません。

サイトがCDNで提供されることを意図している場合、サーバーの負荷を軽減しエンドユーザーに迅速な応答を提供するために、CDNでのキャッシングの機会を増やすことを、Cookieなどの個人情報を誤ってキャッシングしてしまうことの関連するリスクとともに考慮する必要があります。

しかし強力で複雑であることは、必ずしも難しいことではありません。他のほとんどのものと同様にキャッシュはルールによって制御されており、キャッシュ可能性とプライバシーのベストミックスを提供するために、かなり簡単に定義できます。サイトを定期的に監査し、キャッシュ可能なリソースが適切にキャッシュされていることを確認することをオススメします。

著者