Netlifyのデフォルトキャッシュ設定はどう振る舞う?

Netlifyのキャッシュ設定は"Cache-Control: max-age=0,must-revalidate,public"です。この意味はわかりますか?
2021.01.20

静的配信・サーバーレスバックエンドの Netlify ではデフォルトのブラウザ向けキャッシュ設定は次の通りです。

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

このヘッダーがどのようにキャッシュされるか説明できますか?

「キャッシュはするけど、信頼しないでね(“please cache this content, and then do not trust your cache”) *1」、という意味です。

ウェブパフォーマンスでキャッシュは大事

ウェブサービスでコンテンツのキャッシュ設定は重要です。

正しくキャッシュすると、サーバーの負荷軽減やクライアントの表示速度の改善など、様々なメリットがあります。 一方で、設定を誤ると、古いキャッシュがいつまでも表示されたり、個人的なコンテンツのキャッシュが他人にも利用されてしまうといった不具合にも繋がります。

CDNやLast-Modifiedディレクティブなど、キャッシュ技術は前世紀から存在し、進化し続けてきました。

本記事では、長い歴史のある複雑なキャッシュ仕様のなかから、今風なウェブサービス Netlify が利用するキャッシュ設定

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

に限定して、超ピンポイントに解説します。

Netlify のキャッシュ設定をはどうなっている?

Netlify のサイトトップに HTTP GET リクエストして、レスポンスを確認します。

$ curl -I https://www.netlify.com/

HTTP/2 200
cache-control: public, max-age=0, must-revalidate
content-type: text/html; charset=UTF-8
date: Mon, 18 Jan 2021 18:24:13 GMT
etag: "3efb3ad0a8ab2881aff806f3d3b13d31-ssl"
link: <https://www.netlify.com/>; rel="canonical"
referrer-policy: no-referrer-when-downgrade
strict-transport-security: max-age=31536000; includeSubDomains; preload
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
age: 11706
content-length: 409714
server: Netlify
set-cookie: nf_ab=0.866427; expires=Tue, 18-Jan-2022 21:39:19 GMT; path=/
x-nf-request-id: 51cdec21-0e42-4a13-affd-4b120bde3037-6087705

レスポンスにある

  • cache-control: public, max-age=0, must-revalidate
  • etag: "3efb3ad0a8ab2881aff806f3d3b13d31-ssl"

がブラウザ(クライアント)のキャッシュに関連するヘッダーです。

キャッシュしても良いけど(public)、即時に古くなるので(max-age=0)、利用する前にコンテンツが変わっていないこと(etag)を検証してね(must-revalidate)、ということを意味します。

ディレクティブごとに意味を確認します。

Cache-Control: public

MDN によると"The response may be stored by any cache, even if the response is normally non-cacheable" とあります。

サーバーレスポンスをキャッシュするレイヤーは

  • クライアント(ブラウザ)
  • クライアント-サーバー間にあるCDN/プロキシサーバーといったキャッシュ層(shared cache)

の2レイヤーがあります。

"any cache" というのは、このどちらのレイヤーでもキャッシュして良いことを意味します。

private の場合、クライアントだけがキャッシュ可能です。

Cache-Control: max-age

ブラウザは、コンテンツをキャッシュ後、max-age で指定された秒数だけローカルキャッシュを利用できます。

よく似たディレクティブに s-maxage(s-max-age ではないことに注意)があります。 このディレクティブは、CDN/プロキシといった shared cache の保持期間を定義します。

Netlify の場合は max-age=0 のため、キャッシュは即時に古く(stale)なります。キャッシュが max-age を超えて stale になったらどうすればよいのか? そこを定義しているのが、次に紹介する must-revalidate です。

Cache-Control: must-revalidate

must-revalidateの場合、サーバーに問い合わせてコンテンツが変わっていないことが確認できた場合のみ(re-validate)、キャッシュを利用できます。

コンテンツが変わっていないことを確認するために利用するのが、 レスポンスヘッダーに含まれる etag です。 etag はコンテンツのハッシュ値です。

サーバーに対して etag の値を利用して条件付きリクエストを行います。

リクエストヘッダーの if-non-match に etag の値を渡して、条件付きリクエストをします。

# 初回リクエスト
$ curl -I https://www.netlify.com/

HTTP/2 200
cache-control: public, max-age=0, must-revalidate
content-type: text/html; charset=UTF-8
date: Mon, 18 Jan 2021 19:29:28 GMT
etag: "3efb3ad0a8ab2881aff806f3d3b13d31-ssl"
...

# 2回目のリクエスト
$ curl -I https://www.netlify.com/ \
  -H 'if-none-match:"3efb3ad0a8ab2881aff806f3d3b13d31-ssl"'

HTTP/2 304
date: Tue, 19 Jan 2021 13:57:25 GMT
etag: "3efb3ad0a8ab2881aff806f3d3b13d31-ssl"
cache-control: public, max-age=0, must-revalidate
server: Netlify
x-nf-request-id: 6b1423cb-32bf-4f87-846d-18c13d37e6f9-33048218

HTTP レスポンス 304 : Not Modified から、ブラウザキャッシュを利用して大丈夫です。

Cache-Control:no-cache はキャッシュしないわけではない

Cache-Control: max-age=0, must-revalidateCache-Control: no-cache と書いても同じです。 これまでの説明のように、no-cache の字面とは裏腹に、キャッシュするのでご注意ください。

キャッシュしないのは Cache-Control: no-store です。

must-revalidate が存在しない場合

must-revalidateが存在せず、 Cache-Control: max-age=0 だけの場合はどうなるでしょうか?

MDN によると、サーバーが落ちているなどして validate できなかった場合、古いキャッシュが利用されることがあります *2。 このあたりの挙動は、ブラウザ依存と思われます。

バリデーションに失敗した時は古くなったキャッシュを返す *3 stale-if-error というディレクティブも用意されていますが、Chrome/Firefox ですら正式対応していません。

毎回サーバーに validate するのは無駄なのでは?

index.html ファイルなどは、上述の must-revalidate 方式で良いかもしれませんが、JavaScriptなどのアセット系ファイルは更新頻度がレアのため、都度 revalidate が発生するのはもったいないです。

このようなあまり変わらない(immutableな)アセットの場合、webpack のようなビルドツールでファイル名にハッシュ値を含めてリリース間でURLが重複しないようにし、キャッシュ期間(max-age)を長くし、バリデーション無しにブラウザキャッシュを積極的に活用するようにします。

実際、Netlify公式サイト内のコンテンツを確認すると

  • https://cdn.netlify.com/js/2a9d26f41138b72b65d1b93887839f96e34c5511/blog.js
  • https://cdn.netlify.com/bundles/771044017fb819f862d8fe5b1af755ef5239ab22.js

というような URL が存在します。

これらに対してリクエストすると、cache-control: public, max-age=31556926 というように、約1年キャッシュする設定がかえってきました。

$ curl -I https://cdn.netlify.com/js/2a9d26f41138b72b65d1b93887839f96e34c5511/blog.js
HTTP/2 200
accept-ranges: bytes
access-control-allow-origin: *
cache-control: public, max-age=31556926
...

キャッシュ設定は難しい

Netlifyのキャッシュ設定を例に、を中心としたブラウザキャッシュ設定について解説しました。

ブラウザキャッシュに限っても、今回紹介した Cache-Control 以外にも、ExpiresLast-Modified など複数の方法があります。

ブラウザキャッシュ以外にも、CDNやサーバーサイドでもキャッシュ可能です。

要件に合わせて、正しい層で然るべき設定をしましょう。

参照

Cache-Control 周りをより深く学びたい人のためのリンク集です。

脚注

  1. https://www.netlify.com/blog/2017/02/23/better-living-through-caching/
  2. Following may serve stale resource if server is down or lose connectivity.
  3. 多くの人がほしいのはこれ