michimani.net

[Hugo] 静的サイトのキャッシュ戦略について

2020-03-27

このブログは静的サイトジェネレータの Hugo を使ってビルドしています。静的サイトの魅力といえば、表示の速さです。また、CloudFront などの CDN でコンテンツをキャッシュすることで、より表示スピードを上げることができます。今回は静的サイトにおけるキャッシュ戦略について、実際にこのブログで運用している方法を例に紹介したいと思います。

目次

前提

このブログは下記のサービスを用いて AWS 環境上で運用しています。

デプロイの詳細については下記の記事を参照してください。

キャッシュの種類

静的サイトだけでなく Web サイトに対するキャッシュとしては、大きく分けて下記の二種類のキャッシュがあります。

サーバサイドキャッシュには、 CDN のキャッシュの他、データベースキャッシュなどが該当します。クライアントサイドのキャッシュとしては、 cookie、ブラウザのローカルキャッシュなどが該当します。

今回は上記の大きな分類それぞれから CDN のキャッシュブラウザのローカルキャッシュ についての戦略を考えてみます。

コンテンツ・データの種類

キャッシュするコンテンツ・データにも下記のような種類があります。

それぞれキャッシュが残っていることで、古い情報が表示され続けていたり、表示が崩れていたり、ボタンなどの動作が正しくなかったりといったことが起こります。場合によってはコンテンツ・データの種類によってキャッシュ戦略を変える必要も出てきます。

静的サイトのキャッシュ戦略

静的サイトでは、その名の通りデプロイ時以外にコンテンツ情報が更新されることはないため、できる限りキャッシュの恩恵を受けることが望ましいです。そのため、基本戦略として CDN 、 ブラウザともにキャッシュ時間は長くしておくのがよいでしょう。

ただし注意したいのは、 CDN のキャッシュは管理者 (サイト運営者) 側で削除することができますが、ブラウザのローカルキャッシュは削除できないということです。ブラウザのローカルキャッシュはサイトを閲覧しているユーザの端末に保存されるキャッシュなので、ブラウザのスーパーリロードを実施してもらったり、その他の方法でキャッシュを削除してもらうようユーザに依頼する必要があります。

これらを踏まえて、実際にこのブログで運用しているキャッシュ戦略について紹介します。

CDN (CloudFront) のキャッシュ戦略

CloudFront では、 Behavior はデフォルト (*) のみで、キャッシュ時間は下記のように設定しています。

各キャッシュ時間の適用については下記の公式ドキュメントを参照してください。簡単に書いておくと、オリジンの Cache-ControlExpires ヘッダ の値との関係で、 Minimum, Maximum, Default の値がキャッシュ時間として適用されます。

また、 CloudFront では クエリ文字列も含めてキャッシュするかを設定することができます。このブログでは c というクエリパラメータのみキャッシュ対象とするように設定しています。具体的には Query String Forwarding and Caching の項目で Forward all, cache based on whitelist を選択して、 Query String Whitelistc を入力しています。

CloudFront behavior query string

こうすることで、下記のコンテンツはそれぞれ別々にキャッシュされることになります。

一方で、他のクエリ文字列では同一のコンテンツとして扱われます。

また、静的サイトということもありリクエスト時のヘッダ情報や Cookie によるリクエストの違いでキャッシュする必要がないので、リクエストヘッダでのキャッシュ (Cache Based on Selected Request Headers) と Cookie でのキャッシュ (Forward Cookies) は None にしています。

ブラウザのローカルキャッシュ戦略

ブラウザのローカルキャッシュ戦略として、基本的には キャッシュさせない ようにしています。ブラウザのローカルキャッシュを保存させないようにするためには、レスポンスヘッダに Cache-Control でキャッシュの動作を指定する必要があります。

キャッシュさせたくない場合は Cache-Control: no-store を指定し、時間を指定してキャッシュさせる場合は Cache-Control: public, max-age=31536000 のように指定します。

このブログでは、 html と その他のアセット (画像、css、js) でそれぞれ下記のように設定しています。

オリジンに対して max-age を指定する場合、 前述した CloudFront の Minimum, Maximum, Default の値との関係に注意する必要があります。詳しくは下記の公式ドキュメントを参照してください。

データの種類でキャッシュ方法を分ける理由

html とその他アセットでキャッシュ方法を分けているのは、下記の理由からです。

html (ブログ記事本文) については、誤字などがあった際にすぐに変更が反映されてほしいので、ブラウザのローカルキャッシュはしないようにしています。

一方でその他のアセット類については、ブログ記事本文ほど頻繁に更新が発生するものでもないこと、また、特に画像ファイルなどはサイズも大きくなりがりなので、表示の高速化のためにもブラウザのローカルキャッシュをするようにしています。

ただしこれらアセット類に更新があった際には、サイトのスタイルが崩れたり動作が正しくなかったりと、閲覧に支障をきたします。その対策として、各アセットデータには先述したクエリパラメータを付与しています。アセット類を更新した際にクエリパラメータの値も一緒に更新することで、ブラウザは更新後のデータを取得するようになり、新しいデータをローカルキャッシュに保存するようになります。

オリジンの Cache-Control ヘッダ設定

オリジンの Cache-Control について書きましたが、 S3 バケットをオリジンとして指定している場合はどのように設定すればよいのでしょうか。
このブログでは Hugo のビルドと S3 バケットのデプロイを CodeBuild 内で完結しています。実際には下記のような buildspec.yml を使用しています。(一部マスクしています)

version: 0.2

phases:
  install:
    commands:
      - curl -Ls https://github.com/gohugoio/hugo/releases/download/v0.68.3/hugo_0.68.3_Linux-64bit.tar.gz -o /tmp/hugo.tar.gz
      - tar xf /tmp/hugo.tar.gz -C /tmp
      - mv /tmp/hugo /usr/bin/hugo
      - rm -rf /tmp/hugo*
  build:
    commands:
      - hugo
  post_build:
    commands:
      - aws s3 sync "public/" "s3://<target-bucket-name>" --delete --metadata-directive "REPLACE" --cache-control "public, max-age=1209600" --exclude "index.html" --exclude "post/*" --exclude "tags/*" --exclude "archives/*" --exclude "categories/*" --exclude "about/*"
      - aws s3 sync "public/" "s3://<target-bucket-name>" --delete --metadata-directive "REPLACE" --cache-control "no-store" --exclude "*" --include "index.html" --include "post/*" --include "tags/*" --include "archives/*" --include "categories/*" --include "about/*"
      - aws cloudfront create-invalidation --distribution-id YOUR-DISTRIBUTION-ID --paths "/*"

post_build 内で 3 つのコマンドを実行しています。最初の 2 つは S3 バケットへのデプロイで、 3 つ目は CloudFront の Invalidation を作成しています。

S3 バケット内のオブジェクトに対して Cache-Control を設定するには s3 sync コマンドの --metadata-directive オプションを使用します。 Cache-Control だけでなく、その他のレスポンスヘッダを付与したい場合にも使用します。

--exclude および --include オプションを使用することで、 S3 バケットへのデプロイを html とその他アセット類を別々にデプロイし、それぞれ先述した Cache-Control を設定するようにしています。

まとめ

静的サイトのおけるキャッシュ戦略について、 Hugo + AWS で運用しているこのブログを例にして書いてみました。
静的サイトだとシンプルですが、動的サイトになるとデータベースのキャッシュなども考える必要があるので、あらためてキャッシュって怖いなというのが率直な感想です。

もし Hugo やその他の静的サイトでキャッシュどうしようか考えている方がいれば参考にしてみてください。それおかしいやろ等のご意見もいただけると幸いです。

以上、よっしー (michimani) でした。