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

キャッシング

導入

キャッシングは、以前にダウンロードしたコンテンツの再利用を可能にする手法です。コストのかかるネットワークリクエストを回避することにより、パフォーマンスが大幅に向上します。また、Webサイトのオリジンインフラストラクチャへのトラフィックを削減することで、アプリケーションの拡張にも役立ちます。「最速のリクエストはあなたがする必要のないものです」という古い格言があり、キャッシュはリクエストを行わなくて済むようにするための重要な方法の1つです。

Webコンテンツのキャッシュには、3つの基本原則があります。可能な限りキャッシュする、できる限りキャッシュする、エンドユーザーのできるだけ近くでキャッシュする。

可能な限りキャッシュする。 キャッシュできる量を検討する場合、レスポンスが静的か動的かを理解することが重要です。静的なレスポンスとして提供される要求は、リソースとそれを要求するユーザーとの間に1対多の関係があるため、通常はキャッシュ可能です。動的に生成されたコンテンツはより微妙である可能性があり、慎重に検討する必要があります。

できるだけ長くキャッシュする。リソースをキャッシュする時間の長さは、キャッシュされるコンテンツの機密性に大きく依存します。バージョン管理されたJavaScriptリソースは非常に長い時間キャッシュされる可能性がありますが、バージョン管理されていないリソースはユーザーが最新バージョンを取得できるように、より短いキャッシュ期間を必要とする場合があります。

エンドユーザーのできるだけ近くでキャッシュする。エンドユーザーの近くでコンテンツをキャッシュすると、待ち時間がなくなり、ダウンロード時間が短縮されます。たとえば、リソースがエンドユーザーのブラウザにキャッシュされている場合、リクエストはネットワークに送信されず、ダウンロード時間はマシンのI/Oと同じくらい高速です。初めての訪問者、またはキャッシュにエントリがない訪問者の場合、通常、キャッシュされたリソースが返される場所はCDNになります。ほとんどの場合、オリジンサーバーと比較して、ローカルキャッシュかCDNからリソースをフェッチする方が高速です。

通常、Webアーキテクチャには複数のキャッシュ層が含まれます。たとえば、HTTPリクエストは次の場所にキャッシュされる可能性があります。

  • エンドユーザーのブラウザ
  • ユーザーのブラウザーのService Workerキャッシュ
  • 共有ゲートウェイ
  • エンドユーザーに近い側でキャッシュする機能を提供するCDN
  • バックエンドの仕事長を削減するための、アプリケーションの前のキャッシングプロキシ
  • アプリケーション層とデータベース層

この章では、Webブラウザー内でリソースがキャッシュされる方法について見ていきましょう。

HTTPキャッシングの概要

HTTPクライアントがリソースをキャッシュするには、2つの情報を理解する必要があります。

  • 「これをキャッシュできる期間はどれくらいですか?」
  • 「コンテンツがまだ新しいことを検証するにはどうすればよいですか?」

Webブラウザーがクライアントにレスポンスを送信するとき、通常リソースにキャッシュ可能か、キャッシュする期間、リソースの古さを示すヘッダーが含まれます。 RFC 7234は、これをセクション4.2(新しさ)および4.3(検証)でより詳細にカバーしています。

通常、有効期間を伝えるために使用されるHTTPレスポンスヘッダーは次のとおりです。

  • Cache-Control キャッシュの生存期間(つまり、有効期間)を設定できます。
  • Expires 有効期限の日付または時刻を提供します(つまり、期限切れになるとき)。

Cache-Control 両方が存在する場合に優先されます。これらについては、以下で詳しく説明します

キャッシュ内に保存された応答を検証するためのHTTPレスポンスヘッダー、つまりサーバー側で比較するため、条件付き要求を提供するHTTPレスポンスヘッダーは次のとおりです。

  • Last-Modified オブジェクトが最後に変更された日時を示します。
  • エンティティタグ (ETag) コンテンツの一意の識別子を提供します。

ETag 両方が存在する場合に優先されます。これらについては、以下で詳しく説明します。

以下の例には、HTTP Archiveのmain.jsファイルからのリクエスト/レスポンスヘッダーの抜粋が含まれています。これらのヘッダーは、リソースを43,200秒(12時間)キャッシュでき、最後は2か月以上前に変更されたことを示します(Last-ModifiedヘッダーとDateヘッダーの違い)。

> GET /static/js/main.js HTTP/1.1
> Host: httparchive.org
> User-agent: curl/7.54.0
> Accept: */*

< HTTP/1.1 200
< Date: Sun, 13 Oct 2019 19:36:57 GMT
< Content-Type: application/javascript; charset=utf-8
< Content-Length: 3052
< Vary: Accept-Encoding
< Server: gunicorn/19.7.1
< Last-Modified: Sun, 25 Aug 2019 16:00:30 GMT
< Cache-Control: public, max-age=43200
< Expires: Mon, 14 Oct 2019 07:36:57 GMT
< ETag: "1566748830.0-3052-3932359948"

RedBot.orgというツールにURLを入力すると、レスポンスのヘッダーを元としたキャッシュ方法の詳細な説明が表示できます。たとえば、上記のURLのテストは次のような内容を出力します。

ロボットからの Cache-Control 情報
リソースがいつ変更されたか、キャッシュがそれを保存できるかどうか、リソースが新鮮であると見なされる期間、および警告に関する詳細情報を示すRedbotの応答例。
図16.1. ロボットからの Cache-Control 情報

レスポンスにキャッシュヘッダーが存在しない場合、クライアントはレスポンスをヒューリスティクスにキャッシュできます。ほとんどのクライアントは、RFCの推奨ヒューリスティックバリエーションを実装します。これは、Last-Modifiedから経過した時間の10%です。ただし、レスポンスを無期限にキャッシュする可能性もあります。そのため、特定のキャッシュルールを設定して、キャッシュ可能性を確実に制御することが重要です。

レスポンスの72%はCache-Controlヘッダーで提供され、レスポンスの56%はExpiresヘッダーで提供されます。ただし、レスポンスの27%はどちらのヘッダーも使用していないため、ヒューリスティックキャッシュの対象となります。これは、デスクトップとモバイルサイトの両方で一貫しています。

HTTP Cache-Control および Expires ヘッダーの存在
リクエストの72%がCache-Controlヘッダーを使用し、56%がExpiresを使用し、27%がどちらも使用しないことを示す、モバイルとデスクトップの棒グラフ。
図16.2. HTTP Cache-Control および Expires ヘッダーの存在

キャッシュするコンテンツの種類は何ですか?

キャッシュ可能なリソースは、クライアントによって一定期間保存され、後続のリクエストで再利用できます。すべてのHTTPリクエスト全体で、レスポンスの80%はキャッシュ可能と見なされます。つまり、キャッシュがそれらを格納することを許可されています。

  • 要求の6%のTime To Time(TTL)は0秒で、キャッシュされたエントリはすぐに無効になります。
  • 27%はCache-Controlヘッダーがないため、ヒューリスティックにキャッシュされます。
  • 47%は0秒以上キャッシュされます。

残りのレスポンスは、ブラウザーのキャッシュに保存できません。

キャッシュ可能なレスポンスの分布。
デスクトップレスポンスの20%がキャッシュ不可、47%が0秒以上のキャッシュ、27%がヒューリスティックにキャッシュ、6%が0秒のTTLを示す積み上げ棒グラフ。モバイルの統計は非常に似ています(19%、47% 、27%および7%)
図16.3. キャッシュ可能なレスポンスの分布。

次の表は、デスクトップリクエストのキャッシュTTL値をタイプ別に詳細に示しています。ほとんどのコンテンツタイプはキャッシュされますが、CSSリソースは高いTTLで一貫してキャッシュされるようです。

デスクトップキャッシュTTLパーセンタイル(時間)
10 25 50 75 90
Audio 12 24 720 8,760 8,760
CSS 720 8,760 8,760 8,760 8,760
Font < 1 3 336 8,760 87,600
HTML < 1 168 720 8,760 8,766
Image < 1 1 28 48 8,760
Other < 1 2 336 8,760 8,760
Script < 1 < 1 1 6 720
Text 21 336 7,902 8,357 8,740
Video < 1 4 24 24 336
XML < 1 < 1 < 1 < 1 < 1
図16.4. リソースタイプ別のデスクトップキャッシュTTLパーセンタイル。

TTLの中央値のほとんどは高いですが、低いパーセンタイルは、見逃されたキャッシングの機会の一部を強調しています。たとえば画像のTTLの中央値は28時間ですが、25パーセンタイルは1〜2時間であり、10パーセンタイルはキャッシュ可能な画像コンテンツの10%が1時間未満キャッシュされることを示します。

以下の図16.5でコンテンツタイプごとのキャッシュ可能性を詳細に調べると、すべてのHTMLレスポンスの約半分がキャッシュ不可と見なされていることがわかります。さらに、画像とスクリプトの16%はキャッシュ不可です。

デスクトップのコンテンツタイプごとのキャッシュ可能性の分布。
キャッシュ不可、0秒を超えたキャッシュ、デスクトップのタイプごとに0秒だけキャッシュの分割を示す積み上げ棒グラフ。小さいがかなりの割合でキャッシュ不可能でHTMLでは最大50%になり、ほとんどはキャッシュが大きく0で、小さいキャッシュは0 TTLです。
図16.5. デスクトップのコンテンツタイプごとのキャッシュ可能性の分布。

モバイルの同じデータを以下に示します。ご覧のとおり、コンテンツタイプのキャッシュ可能性はデスクトップとモバイルで一貫しています。

モバイルのコンテンツタイプ別のキャッシュ可能性の分布。
キャッシュ不可、0秒を超えたキャッシュ、デスクトップのタイプごとに0秒だけキャッシュの分割を示す積み上げ棒グラフ。小さいがかなりの割合でキャッシュ不可能でHTMLでは最大50%になり、ほとんどはキャッシュが大きく0で、小さいキャッシュは0 TTLです。
図16.6. モバイルのコンテンツタイプ別のキャッシュ可能性の分布。

Cache-ControlExpires

HTTP/1.0では、Expiresヘッダーは、レスポンスが古くなったと見なされる日時を示すために使用されました。その値は、次のような日付のタイムスタンプです。

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

HTTP/1.1はCache-Controlヘッダーを導入し、最新のクライアントのほとんどは両方のヘッダーをサポートしています。このヘッダーは、キャッシングディレクティブを介して、はるかに高い拡張性を提供します。例えば。

  • no-store リソースをキャッシュしないことを示すために使用できます。
  • max-age 鮮度の寿命を示すために使用できます。
  • must-revalidate キャッシュされたエントリは、使用する前に条件付きリクエストで検証する必要があることをクライアントに伝えます。
  • private レスポンスはブラウザによってのみキャッシュされ、複数のクライアントにサービスを提供する仲介者によってキャッシュされるべきではないことを示します。

HTTPレスポンスの53%は、max-ageディレクティブを持つCache-Controlヘッダーが含まれ、54%はExpiresヘッダーが含まれます。ただし、これらのレスポンスの41%のみが両方のヘッダーを使用します。つまり、レスポンスの13%が古いExpiresヘッダーのみに基づいてキャッシュされます。

Cache-Control と Expires ヘッダーの使用法。
レスポンスの53%を示す棒グラフには、「Cache-Control:max-age」、54%-55%が「Expires」、41%-42%が両方を使用し、34%がどちらも使用していません。数字は、デスクトップとモバイルの両方について示されていますが、有効期限の使用率が高いモバイルとほぼ同じです。
図16.7. Cache-ControlExpires ヘッダーの使用法。

Cache-Controlディレクティブ

HTTP/1.1仕様には、Cache-Controlレスポンスヘッダーで使用できる複数のディレクティブが含まれており、以下で詳しく説明します。1つのレスポンスで複数を使用できることに注意してください。

ディレクティブ 説明
max-age リソースをキャッシュできる秒数を示します。
public 任意のキャッシュにレスポンスを保存できます。
no-cache キャッシュされたエントリは、使用する前に再検証する必要があります。
must-revalidate 古いキャッシュエントリは、使用する前に再検証する必要があります。
no-store レスポンスがキャッシュ不可能なことを示します。
private レスポンスは特定のユーザー向けであり、共有キャッシュに保存しない。
no-transform このリソースに対して変換を行わないでください。
proxy-revalidate must-revalidateと同じですが、共有キャッシュに適用されます。
s-maxage max ageと同じですが、共有キャッシュにのみ適用されます。
immutable キャッシュされたエントリは決して変更されず、再検証は不要であることを示します。
stale-while-revalidate クライアントがバックグラウンドで新しいレスポンスを非同期にチェックしながら、古いレスポンスを受け入れようとしていることを示します。
stale-if-error 新しいレスポンスのチェックが失敗した場合に、クライアントが古いレスポンスを受け入れる意思があることを示します。
図16.8. Cache-Control ディレクティブ。

たとえば、cache-control:public、max-age = 43200は、キャッシュされたエントリを43,200秒間保存し、共有キャッシュに保存できることを示します。

モバイルでの Cache-Control ディレクティブの使用。
15のcache-controlディレクティブとその使用量の棒グラフ。74.8%のmax-age、37.8%のpublic、27.8%のno-cache、18%のno-store、14.3%のprivate、3.4%のimmutable、3.3%のno-transform、2.4%のstale-while-revalidate、2.2%のpre-check、2.2%のpost-check、1.9%のs-maxage、1.6%のproxy-revalidate、0.3%set-cookieおよび0.2%のstale-if-error。統計は、デスクトップとモバイルでほぼ同じです。
図16.9. モバイルでの Cache-Control ディレクティブの使用。

上記の図16.9は、モバイルWebサイトで使用されている上位15のCache-Controlディレクティブを示しています。デスクトップとモバイルの結果は非常に似ています。これらのキャッシュディレクティブの人気について、興味深い観察結果がいくつかあります。

  • max-ageCache-Controlヘッダーのほぼ75%で使用され、no-storeは18%で使用されます。
  • privateが指定されない限り、キャッシュされたエントリはpublicであると想定されるため、publicが必要になることはほとんどありません。回答の約38%にpublicが含まれています。
  • immutableディレクティブは比較的新しく、2017年に導入され、FirefoxおよびSafariでサポートされています。その使用率は3.4%に拡大し、Facebook、Googleのサードパーティのレスポンスで広く使用されています。

このリストに表示される別の興味深いディレクティブセットは、pre-checkpost-checkです。これらは、Cache-Controlレスポンスヘッダーの2.2%(約780万件のレスポンス)で使用されます。このヘッダーのペアは、バックグラウンドで検証を提供するためにInternet Explorer 5で導入されたものですが、Webサイトによって正しく実装されることはほとんどありませんでした。これらのヘッダーを使用したレスポンスの99.2%は、pre-check=0post-check=0の組み合わせを使用していました。これらのディレクティブの両方が0に設定されている場合、両方のディレクティブは無視されます。したがって、これらのディレクティブは正しく使用されなかったようです!

ロングテールでは、レスポンスの0.28%で1,500を超える間違ったディレクティブが使用されています。これらはクライアントによって無視され、「nocache」「s-max-age」「smax-age」「maxage」などのスペルミスが含まれます。「max-stale」「proxy-public」「surrogate-control」など存在しないディレクティブも多数あります。

Cache-Control: no-store, no-cache and max-age=0

レスポンスがキャッシュ可能でない場合、Cache-Control no-storeディレクティブを使用する必要があります。このディレクティブを使用しない場合、レスポンスはキャッシュ可能です。

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

  • Cache-Control: no-cacheの設定は、リソースがキャッシュできないように聞こえるかもしれません。ただし、no-cacheディレクティブでは、使用する前にキャッシュされたエントリを再検証する必要があり、キャッシュ不可と同じではありません。
  • Cache-Control: max-age = 0を設定すると、TTLが0秒に設定されますが、これはキャッシュ不可と同じではありません。 max-ageを0に設定すると、リソースはブラウザーのキャッシュに保存され、すぐに無効になります。これにより、ブラウザは条件付きリクエストを実行してリソースの新しさを検証する必要があります。

機能的には、no-cachemax-age=0は似ています。どちらもキャッシュされたリソースの再検証を必要とするためです。 no-cacheディレクティブは、0より大きいmax-ageディレクティブと一緒に使用することもできます。

300万を超えるレスポンスには、no-storeno-cachemax-age=0の組み合わせが含まれます。これらのディレクティブのうち、no-storeが優先され、他のディレクティブは単に冗長です。

レスポンスの18%にはno-storeが含まれ、レスポンスの16.6%にはno-storeno-cacheの両方が含まれます。no-storeが優先されるため、リソースは最終的にキャッシュ不可になります。

max-age=0ディレクティブは、no-storeが存在しないレスポンスの1.1%(400万を超えるレスポンス)に存在します。これらのリソースはブラウザにキャッシュされますが、すぐに期限切れになるため、再検証が必要になります。

キャッシュTTLとリソースの経過時間はどのように比較されますか?

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

キャッシュTTLを選択するときは、「これらのアセットをどのくらいの頻度で更新しますか?」と自問してください。そして「彼らのコンテンツの感度は何ですか?」。たとえば、ヒーローのイメージがたまに更新される場合、非常に長いTTLでキャッシュします。 JavaScriptリソースが頻繁に変更されることが予想される場合は、バージョン管理して長いTTLでキャッシュするか、短いTTLでキャッシュします。

以下のグラフは、コンテンツタイプごとのリソースの相対的な年を示しています。ここで、より詳細な分析を読むことができます。 HTMLは最も短い年齢のコンテンツタイプである傾向があり、伝統的にキャッシュ可能なリソース(スクリプトCSS、およびフォント)の非常に大きな割合が1年以上古いです!

コンテンツタイプ別のリソース年分布。
コンテンツの年を示すスタック棒グラフ。0〜52週に分割され、1年以上と2年以上で、nullと負の数字も表示されます。統計は、ファーストパーティとサードパーティに分かれています。値0は、特にファーストパーティのHTML、テキスト、およびxml、およびすべてのアセットタイプのサードパーティリクエストの最大50%に使用されます。中間年を使用し、その後1年と2年の間かなりの使用量を使用するミックスがあります。
図16.10. コンテンツタイプ別のリソース年分布。

リソースのキャッシュ可能性をその経過時間と比較することにより、TTLが適切であるか短すぎるかを判断できます。たとえば、以下のレスポンスによって提供されるリソースは、2019年8月25日に最後の変更がされました。つまり、配信時に49日経過していました。 Cache-Controlヘッダーは、43,200秒(12時間)キャッシュできることを示しています。より長いTTLが適切かどうかを調査するのに十分古いものです。

< HTTP/1.1 200
< Date: Sun, 13 Oct 2019 19:36:57 GMT
< Content-Type: application/javascript; charset=utf-8
< Content-Length: 3052
< Vary: Accept-Encoding
< Server: gunicorn/19.7.1
< Last-Modified: Sun, 25 Aug 2019 16:00:30 GMT
< Cache-Control: public, max-age=43200
< Expires: Mon, 14 Oct 2019 07:36:57 GMT
< ETag: "1566748830.0-3052-3932359948" 

全体的に、Webで提供されるリソースの59%のキャッシュTTLは、コンテンツの年齢に比べて短すぎます。さらに、TTLと経過時間のデルタの中央値は25日です。

これをファーストパーティとサードパーティで分けると、ファーストパーティのリソースの70%がより長いTTLの恩恵を受けることもわかります。これは、キャッシュ可能なものに特に注意を払い、キャッシュが正しく構成されていることを確認する必要があることを明確に強調しています。

クライアント ファーストパーティ サードパーティ 全体
デスクトップ 70.7% 47.9% 59.2%
モバイル 71.4% 46.8% 59.6%
図16.11. TTLが短いリクエストの割合。

鮮度の検証

キャッシュ内に格納されたレスポンスの検証に使用されるHTTPレスポンスヘッダーは、Last-ModifiedおよびETagです。 Last-Modifiedヘッダーは、その名前が示すとおりに機能し、オブジェクトが最後に変更された時刻を提供します。 ETagヘッダーは、コンテンツの一意の識別子を提供します。

たとえば、以下のレスポンスは2019年8月25日に変更され、「1566748830.0-3052-3932359948」ETag値があります。

< HTTP/1.1 200
< Date: Sun, 13 Oct 2019 19:36:57 GMT
< Content-Type: application/javascript; charset=utf-8
< Content-Length: 3052
< Vary: Accept-Encoding
< Server: gunicorn/19.7.1
< Last-Modified: Sun, 25 Aug 2019 16:00:30 GMT
< Cache-Control: public, max-age=43200
< Expires: Mon, 14 Oct 2019 07:36:57 GMT
< ETag: "1566748830.0-3052-3932359948"

クライアントは、If-Modified-Sinceという名前のリクエストヘッダーのLast-Modified値を使用して、キャッシュされたエントリを検証する条件付きリクエストを送信できます。同様に、If-None-Matchリクエストヘッダーを使用してリソースを検証することもできます。これは、クライアントがキャッシュ内のリソースに対して持っているETag値に対して検証します。

以下の例では、キャッシュエントリはまだ有効であり、HTTP 304がコンテンツなしで返されました。これにより、リソース自体のダウンロードが保存されます。キャッシュエントリが最新ではない場合、サーバーは200で更新されたリソースを応答し、再度ダウンロードする必要があります。

> GET /static/js/main.js HTTP/1.1
> Host: www.httparchive.org
> User-Agent: curl/7.54.0
> Accept: */*
> If-Modified-Since: Sun, 25 Aug 2019 16:00:30 GMT

< HTTP/1.1 304
< Date: Thu, 17 Oct 2019 02:31:08 GMT
< Server: gunicorn/19.7.1
< Cache-Control: public, max-age=43200
< Expires: Thu, 17 Oct 2019 14:31:08 GMT
< ETag: "1566748830.0-3052-3932359948"
< Accept-Ranges: bytes

全体としてレスポンスの65%はLast-Modifiedヘッダーで、42%はETagで提供され、38%は両方を使用します。ただし、レスポンスの30%にはLast-Modifiedヘッダー、ETagヘッダーは含まれていません。

デスクトップWebサイトの Last-Modified および ETag ヘッダーを介した鮮度の検証の採用。
デスクトップリクエストの64.4%が最後に変更され、42.8%がETagを持ち、37.9%が両方を持ち、30.7%がどちらも持たないことを示す棒グラフ。モバイルの統計は、最終変更が65.3%、ETagが42.8%、両方が38.0%、どちらも29.9%というほぼ同じです。
図16.12. デスクトップWebサイトの Last-Modified および ETag ヘッダーを介した鮮度の検証の採用。

日付文字列の有効性

タイムスタンプの伝達に使用されるHTTPヘッダーがいくつかあり、これらの形式は非常に重要です。 Dateレスポンスヘッダーは、リソースがいつクライアントに提供されたかを示します。 Last-Modifiedレスポンスヘッダーは、サーバーでリソースが最後に変更された日時を示します。また、Expiresヘッダーは、(Cache-Controlヘッダーの存在しない場合)リソースがキャッシュ可能な期間を示すために使用されます。

これら3つのHTTPヘッダーはすべて、日付形式の文字列を使用してタイムスタンプを表します。

例えば。

> GET /static/js/main.js HTTP/1.1
> Host: httparchive.org
> User-Agent: curl/7.54.0
> Accept: */*

< HTTP/1.1 200
< Date: Sun, 13 Oct 2019 19:36:57 GMT
< Content-Type: application/javascript; charset=utf-8
< Content-Length: 3052
< Vary: Accept-Encoding
< Server: gunicorn/19.7.1
< Last-modified: Sun, 25 Aug 2019 16:00:30 GMT
< Cache-Control: public, max-age=43200
< Expires: Mon, 14 Oct 2019 07:36:57 GMT
< ETag: "1566748830.0-3052-3932359948"

ほとんどのクライアントは、無効な日付文字列を無視します。これにより、提供されているレスポンスを無視します。これは、誤ったLast-ModifiedヘッダーがLast-Modifiedタイムスタンプなしでキャッシュされるため、条件付きリクエストを実行できなくなるため、キャッシュ可能性に影響を与える可能性があります。

通常、Date HTTPレスポンスヘッダーは、クライアントにレスポンスを提供するWebサーバーまたはCDNによって生成されます。ヘッダーは通常、サーバーによって自動的に生成されるため、エラーが発生しにくい傾向はあります。これは、無効なDateヘッダーの割合が非常に低いことを反映しています。 Last-Modifiedヘッダーは非常に類似しており、無効なヘッダーは0.67%のみでした。しかし、驚いたのは、3.64%のExpiresヘッダーが無効な日付形式を使用していたことです!

レスポンスヘッダーの無効な日付形式。
デスクトップレスポンスの0.10%に無効な日付があり、0.67%に無効なLast-Modifiedがあり、3.64%に無効なExpiresがあることを示す棒グラフ。モバイルの統計は非常によく似ており、レスポンスの0.06%に無効な日付があり、0.68%に無効なLast-Modifiedがあり、3.50%に無効な有効期限があります。
図16.13. レスポンスヘッダーの無効な日付形式。

Expiresヘッダーの無効な使用の例は次のとおりです。

  • 有効な日付形式ですが、GMT以外のタイムゾーンを使用しています

  • 0や-1などの数値

  • Cache-Controlヘッダーで有効な値

    無効なExpiresヘッダーの最大のソースは、人気のあるサードパーティから提供されるアセットからのものです。たとえば、Expires:Tue、27 Apr 1971 19:44:06 ESTなど、日付/時刻はESTタイムゾーンを使用します。

ヘッダーを変更

キャッシングで最も重要な手順の1つは、要求されているリソースがキャッシュされているかどうかを判断することです。これは単純に見えるかもしれませんが、多くの場合、URLだけではこれを判断するには不十分です。たとえば同じURLのリクエストは、使用する圧縮(gzip、brotliなど)が異なる場合や、モバイルの訪問者に合わせて変更および調整できます。

この問題を解決するために、クライアントはキャッシュされた各リソースに一意の識別子(キャッシュキー)を与えます。デフォルトでは、このキャッシュキーは単にリソースのURLですが、開発者はVaryヘッダーを使用して他の要素(圧縮方法など)を追加できます。

Varyヘッダーは、1つ以上の要求ヘッダー値の値をキャッシュキーに追加するようにクライアントに指示します。この最も一般的な例は、Vary:Accept-Encodingです。これにより、Accept-Encodingリクエストヘッダー値(gzipbrdeflateなど)のキャッシュエントリが別になります。

別の一般的な値はVary:Accept-EncodingUser-Agentです。これは、Accept-Encoding値とUser-Agent文字列の両方によってキャッシュエントリを変更するようにクライアントに指示します。共有プロキシとCDNを扱う場合、Accept-Encoding以外の値を使用すると、キャッシュキーが弱められ、キャッシュから提供されるトラフィックの量が減少するため、問題発生の可能性があります。

一般に、そのヘッダーに基づいてクライアントに代替コンテンツを提供する場合のみ、キャッシュを変更する必要があります。

Varyヘッダーは、HTTPレスポンスの39%、およびCache-Controlヘッダーを含むレスポンスの45%で使用されます。

以下のグラフは、上位10個のVaryヘッダー値の人気を示しています。 Accept-EncodingはVaryの使用の90%を占め、User-Agent(11%)、Origin(9%)、Accept(3%)が残りの大部分を占めています。

Varyヘッダーの使用率。
accept-encodingは90%で使用、ユーザーエージェントは10%-11%、オリジンは約7%-8%、acceptは少なく、cookie、x-forward-proto、accept-language、host、x-origin、access-control-request-method、およびaccess-control-request-headersの使用はほとんどありません
図16.14. Varyヘッダーの使用率。

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

レスポンスがキャッシュされると、そのヘッダー全体もキャッシュにスワップされます。これが、DevToolsを介してキャッシュされたレスポンスを検査するときにレスポンスヘッダーを表示できる理由です。

キャッシュされたリソースのChrome開発ツール。
キャッシュされたレスポンスのHTTPレスポンスヘッダーを示すChrome開発者ツールのスクリーンショット。
図16.15. キャッシュされたリソースのChrome開発ツール。

しかし、レスポンスにSet-Cookieがある場合はどうなりますか? RFC 7234セクション8によると、Set-Cookieレスポンスヘッダーはキャッシングを禁止しません。これは、キャッシュされたエントリがSet-Cookieでキャッシュされている場合、それが含まれている可能性があることを意味します。 RFCでは、適切なCache-Controlヘッダーを構成して、レスポンスのキャッシュ方法を制御することを推奨しています。

Set-Cookieを使用してレスポンスをキャッシュすることのリスクの1つは、Cookieの値を保存し、後続の要求に提供できることです。 Cookieの目的によっては、心配な結果になる可能性があります。たとえば、ログインCookieまたはセッションCookieが共有キャッシュに存在する場合、そのCookieは別のクライアントによって再利用される可能性があります。これを回避する1つの方法は、Cache-Control``プライベートディレクティブを使用することです。これにより、クライアントブラウザーによるレスポンスのキャッシュのみが許可されます。

キャッシュ可能なレスポンスの3%にSet-Cookieヘッダーが含まれています。これらのレスポンスのうち、プライベートディレクティブを使用しているのは18%のみです。残りの82%には、パブリックおよびプライベートキャッシュサーバーでキャッシュできるSet-Cookieを含む530万のHTTPレスポンスが含まれています。

Set-Cookie レスポンスのキャッシュ可能なレスポンス。
レスポンスの97%を示す棒グラフはSet-Cookieを使用せず、3%が使用します。この3%の内、15.3%がプライベート、84.7%がデスクトップ、モバイルは18.4%がパブリック、81.6%がプライベートであるという別の棒グラフに拡大されています。
図16.16. Set-Cookie レスポンスのキャッシュ可能なレスポンス。

AppCacheおよびService Worker

アプリケーションキャッシュまたはAppCacheはHTML5の機能であり、開発者はブラウザがキャッシュするリソースを指定し、オフラインユーザーが利用できるようにできます。この機能は廃止されており、Web標準からも削除され、ブラウザーのサポートは減少しています。実際、使われているのが見つかった場合、Firefox v44 +は、開発者に対して代わりにService Workerを使用することを推奨していますChrome 70は、アプリケーションキャッシュをセキュリティで保護されたコンテキストのみに制限します。業界では、このタイプの機能をService Workerに実装する方向へ移行しており、ブラウザサポートは急速に成長しています。

実際、HTTP Archiveトレンドレポートの1つは、以下に示すService Workerの採用を示しています。

Service Workerが制御するページの時系列。 (引用: HTTP Archive)
2016年10月から2019年7月までのService Workerが制御するサイトの使用状況を示す時系列チャート。モバイルとデスクトップの両方で使用量は年々着実に増加していますが、依然として両方で0.6%未満です。
図16.17. Service Workerが制御するページの時系列。 (引用: HTTP Archive)

採用率はまだウェブサイトの1%を下回っていますが、2017年1月から着実に増加しています。プログレッシブWebアプリの章では、人気サイトでの使用によりこのグラフが示唆するよりも多く使用されているという事実を含め、上記のグラフでは1回のみカウントされます。

次の表では、AppCacheとService Workerの使用状況の概要を確認できます。 32,292のWebサイトでService Workerが実装されていますが、1,867のサイトでは非推奨のAppCache機能が引き続き使用されています。

Service Workerを使用しない Service Workerを使用する 合計
AppCacheを使用しない 5,045,337 32,241 5,077,578
AppCacheを使用する 1,816 51 1,867
合計 5,047,153 32,292 5,079,445
図16.18. AppCacheを使用するWebサイトとService Workerの数。

これをHTTPとHTTPSで分類すると、さらに興味深いものになります。 581のAppCache対応サイトはHTTP経由で提供されます。つまり、Chromeがこの機能を無効にしている可能性があります。 HTTPSはService Workerを使用するための要件ですが、それらを使用するサイトの907はHTTP経由で提供されます。

Service Workerを使用しない Service Workerを使用する
HTTP AppCacheを使用しない 1,968,736 907
AppCacheを使用する 580 1
HTTPS AppCacheを使用しない 3,076,601 31,334
AppCacheを使用する 1,236 50
図16.19. AppCacheを使用するWebサイト数とHTTP/HTTPSによるService Workerの使用量。

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

GoogleのLighthouseツールを使用すると、ユーザーはWebページに対して一連の監査を実行できます。キャッシュポリシー監査では、サイトが追加のキャッシュの恩恵を受けることができるかどうかを評価します。これは、コンテンツの経過時間(Last-Modifiedヘッダー経由)をキャッシュTTLと比較し、リソースがキャッシュから提供される可能性を推定することによりこれを行います。スコアに応じて、結果にキャッシュの推奨事項が表示され、キャッシュできる特定のリソースのリストが表示される場合があります。

キャッシュポリシーの改善の可能性を強調したLighthouseレポート。
Google Lighthouseツールからのレポートの一部のスクリーンショット。「効率的なキャッシュポリシーを使用した静的アセットの提供」セクションが開いており、多数のリソース、名前が編集された人、キャッシュTTLとサイズのリストが表示されます。
図16.20. キャッシュポリシーの改善の可能性を強調したLighthouseレポート。

Lighthouseは、監査ごとに0%〜100%の範囲のスコアを計算し、それらのスコアは全体のスコアに組み込まれます。キャッシングスコアは、潜在的なバイト節約に基づいています。 Lighthouseの結果を調べると、キャッシュポリシーでどれだけのサイトがうまく機能しているかを把握できます。

モバイルWebページの「Use Long Cache TTL」監査のLighthouseスコアの分布。
積み上げ棒グラフの38.2%のウェブサイトのスコアは10%未満、29.0%のウェブサイトのスコアは10%〜39%、18.7%のウェブサイトのスコアは40%〜79%、10.7%のウェブサイトは80%から99%のスコア、および3.4​​%のWebサイトが100%のスコアを取得します。
図16.21. モバイルWebページの「Use Long Cache TTL」監査のLighthouseスコアの分布。

100%を獲得したサイトは3.4%のみです。これは、ほとんどのサイトがキャッシュの最適化の恩恵を受けることができることを意味します。サイトの圧倒的多数が40%未満で、38%が10%未満のスコアを記録しています。これに基づいて、Webにはかなりの量のキャッシュの機会があります。

Lighthouseは、より長いキャッシュポリシーを有効にすることで、繰り返しビューで保存できるバイト数も示します。追加のキャッシュの恩恵を受ける可能性のあるサイトのうち、82%がページの重みを最大で1MB削減できます。

Lighthouseキャッシング監査による潜在的なバイト節約の分布。
Webサイトの56.8%が1MB未満の潜在的なバイト節約を示す積み上げ棒グラフ、22.1%は1〜2MBの節約、8.3%は2〜3MBの節約になります。 4.3%は3〜4MB節約でき、6.0%は4MB以上節約できました。
図16.22. Lighthouseキャッシング監査による潜在的なバイト節約の分布。

結論

キャッシングは非常に強力な機能であり、ブラウザ、プロキシ、その他の仲介者(CDNなど)がWebコンテンツを保存し、エンドユーザーへ提供できるようにします。これにより、往復時間が短縮され、コストのかかるネットワーク要求が最小限に抑えられるため、パフォーマンス上のメリットは非常に大きくなります。

キャッシュも非常に複雑なトピックです。キャッシュエントリを検証するだけでなく、新鮮さを伝えることができるHTTPレスポンスヘッダーは多数あります。Cache-Controlディレクティブは、非常に多くの柔軟性と制御を提供します。ただし、開発者は、それがもたらす間違いの追加の機会に注意する必要があります。キャッシュ可能なリソースが適切にキャッシュされていることを確認するためにサイトを定期的に監査することをお勧めします。LighthouseREDbotなどのツールは、分析の簡素化に役立ちます。

著者