Cache-Controlのおさらい
今回は、Remixからレスポンスを返す際に、Cache-Controlヘッダーを設定することで、CDNサーバーのキャッシュの挙動を制御してみます。まずは、Cache-Controlヘッダーについておさらいしておきましょう。Cache-Controlヘッダーは、Web標準の一環としてHTTPの仕様で定義されたヘッダーで、ブラウザやCDNサーバーとサーバーの間で、どのようにキャッシュを扱うべきかを指示するために使われます。
指示の内容はディレクティブと呼ばれ、キャッシュを制御するために使われます。Cache-Controlヘッダーに指定できるディレクティブには、いくつかの種類があります。大雑把にジャンル分けすると、キャッシュの有効範囲を指定するディレクティブとキャッシュの有効期限に関するディレクティブの2種類に分けられます。それぞれの挙動について、簡単に説明します。
キャッシュの有効範囲を指定するディレクティブ
キャッシュの有効範囲を指定するディレクティブは、主にprivateとpublicの2つがあります。これらのディレクティブは、キャッシュをどの範囲で利用するかを指定します(リスト1)。
Cache-Control: private Cache-Control: public
privateを指定した場合、キャッシュはプライベートキャッシュ(多くの場合、ブラウザ内のローカルキャッシュ)にのみ保存されます。個人情報を含むコンテンツについてキャッシュしたい場合は、privateを指定する必要があります。
publicを指定した場合、キャッシュは共有キャッシュに保存されます。共有キャッシュは、複数のユーザーで共有されるキャッシュのことで、CDNサーバーが持つキャッシュが該当します。公開情報のコンテンツについてキャッシュしたい場合は、publicを指定します。基本的には、ローカルキャッシュに保存したいか、共有キャッシュに保存したいかで、privateとpublicを使い分けます(図6)。
有効範囲の枠組みでいうと、キャッシュを一切禁止するno-store
もありますね。
キャッシュの有効期限に関するディレクティブ
キャッシュの有効期限に関するディレクティブは、もう少し多様です。基本的な考え方として、キャッシュには「新鮮である(fresh)」と「古くなっている(stale)」の2つの状態があるという考えのもとで設計されています。
ディレクティブでは、新鮮で無くなるまでの時間を指定したり、新鮮でなくなった後の挙動を指定したりすることができます。キャッシュが古くなっていた場合、オリジンサーバーに問い合わせて、新しいコンテンツがないか確認する(=再検証する)ことが多いです。
ディレクティブ | 概要 |
---|---|
max-age=N | N秒後まではキャッシュが新鮮であるとみなす |
s-maxage=N | N秒後までは共有キャッシュが新鮮であるとみなす |
stale-while-revalidate=N | 古くなってからN秒後までの間は、レスポンスを再検証しつつ古いキャッシュを返す |
stale-if-error=N | オリジンサーバーがエラーを返した場合、N秒間は古いキャッシュを返す |
must-revalidate | キャッシュが古くなった場合、オリジンサーバーに再検証を要求する |
proxy-revalidate | 共有キャッシュが古くなった場合、オリジンサーバーに再検証を要求する |
no-cache | キャッシュを保存することはできるが、常にオリジンサーバーに再検証を要求する |
immutable | キャッシュが一度保存されたら、再検証を行わずにキャッシュを使う |
no-transform | キャッシュが保存される際に、コンテンツを変換しないようにする |
表1のimmutable
について、少し補足します。immutable
は、キャッシュが新鮮な間の再検証を拒否するためのディレクティブです。実は、リクエストヘッダーのためのディレクティブがあり、代表的なものがブラウザのリロード操作時に付与されるCache-Control: max-age=0
です。リクエスト時のmax-age=N
は「N秒以内に保存されたキャッシュがあれば再利用してよいが、なければ再検証してからレスポンスを返せ」という指示です。これをゼロ秒でリクエストすると、キャッシュが新鮮であっても再検証を要求することになります。
immutable
は、この再検証を拒否して、新鮮なキャッシュがあれば常に再利用します。時間経過で変化する可能性がないリソースに、immutable
を指定することで、再検証のコストを削減できます。no-transform
も補足しておきましょう。
例えば画像データをレスポンスとして扱う場合、CDNや類似の機能を提供するサービスは、転送サイズを縮小するためなどの目的で、データを加工してから保存することがあります。
no-transform
は、そういった変換処理を行わないように要望するためのディレクティブです。画像データをそのままキャッシュに保存したい場合に指定します。これまでは有効期限を指定する挙動を中心に紹介していましたが、共有キャッシュが古くなった後の挙動を制御するディレクティブも設定できます。
例えば、再検証を終えてからレスポンスを返す must-revalidate
やproxy-revalidate
を使う場合は、リスト2のように指定します。
Cache-Control: public, s-maxage=3600, must-revalidate Cache-Control: public, s-maxage=3600, proxy-revalidate # 共有キャッシュ専用の指定
この場合、キャッシュを保存してから1時間経つと古くなったとみなされ、その後のリクエストに対しては、オリジンサーバーに再検証を要求します。再検証が成功した場合は、新しいコンテンツを返します。再検証が失敗した場合は、エラーになります。
must-revalidate
やproxy-revalidate
では、再検証の間にユーザーを待たせてしまい、場合によってはユーザー体験を大きく損なうため、別の方法も用意されています。キャッシュが古くなっていた場合にも、一旦は古いキャッシュを返しつつ、オリジンサーバーへの再検証の要求も開始しておくstale-while-revalidate
は、ユーザー体験とキャッシュの更新のバランスを取れるディレクティブです(リスト3)。
Cache-Control: public, s-maxage=3600, stale-while-revalidate=7200
この場合、キャッシュを保存してから1時間以内はキャッシュを返すだけの挙動になります。1時間を過ぎた場合は、さらに2時間の間だけ、古いキャッシュを返しつつ、オリジンサーバーに再検証を要求する挙動を行います。
再検証が成功すると、キャッシュが新鮮な状態に戻ります。再検証が失敗した場合には、見た目上何も起こりません。
キャッシュを保存してから3時間(1時間+2時間)経ってからのリクエストに対しては、must-revalidate
と同じように、ユーザーを待たせてオリジンサーバーに再検証を要求します。ユーザーのアクセス頻度に応じて、秒数を調整しておくとよいでしょう。
今回のサンプルについて
それでは、実際にRemixプロジェクトのキャッシュ設定を行ってみましょう。題材として、第3回で作ったHacker News Viewerを、第9回に解説したCloudflare Pages向け構成で再実装したものを利用します(図7)。
ソースコード全体の解説は行いませんが、どんなアプリケーションなのかを要点だけおさらいしておきましょう。
- ソーシャルニュースサイト「Hacker News」の人気記事20件を表示する
- サイドメニューに人気記事20件のリンクを表示する
- サイドメニューのリンクをクリックすると、その記事の内容とコメントを表示する
こんなところでしょうか。今回は、各記事のURL(/top20/:記事ID)
に対応した、SNSシェア用のサムネイル画像を生成するAPIをloaderで作成し、その画像ファイルをキャッシュしようと思います。
Remixプロジェクトの動的コンテンツのキャッシュ設定
では、サンプルコードの現状の挙動を確認してみましょう。通信結果をChromeのDev Toolで表示し、Cf-Cache-Statusヘッダーの値を確認します。まずは、ページを初期表示するときにロードされる、HTMLファイルのキャッシュ状況を確認します(図8)。
Cf-Cache-Statusヘッダーの値がDYNAMICになっていることがわかります。これは、キャッシュが行われない場合の挙動で、常にオリジンサーバーを呼び出して、サーバーサイドレンダリングを行っていることを示しています。
Remixは初期表示の後の画面遷移からAjaxによるデータ取得を行うため、一度、サイドメニューの別の記事のタイトルをクリックして、画面遷移した時の通信内容を確認してみましょう(図9)。
こちらもCf-Cache-Statusヘッダーの値がDYNAMICになっています。Remixの画面遷移に伴う通信は、デフォルトではキャッシュされないようです。Cloudflare公式ドキュメントのキャッシュのデフォルトの挙動を読んでみると、HTMLやJSONはキャッシュしないそうです。RemixのCloudflare Pagesでの挙動も、これに準拠しています。