🔥

Rails におけるブラウザキャッシュ

2024/07/16に公開

はじめに

Rails では Rails.cache のようなサーバーが保持するシンプルなキャッシュストレージの仕組みの他に、ブラウザにキャッシュさせる Cache-Control を使ったキャッシュの仕組みもサポートしています。この機能は条件付きGETと呼ばれているようです。この記事では、この条件付き GET について具体例を交えながら説明します。

なぜブラウザキャッシュを使うのか

ここで言うブラウザキャッシュは文字通り、ブラウザがレスポンスを記憶することで、サーバーとのやりとりを省略することを指します。ブラウザキャッシュはサーバーとの通信をほとんど行わないため、回線が細い場合や、動画や画像などファイルサイズの大きいデータを取り扱っている場合にはブラウザキャッシュは有効に働く可能性があります。

Cache-Control について

まず、ブラウザキャッシュを有効にするには、サーバーが Cache-Control リクエストヘッダーを返す必要があります。そして Rails はデフォルトでこれをセットする仕様になっています。実際に Rails で作ったサービスの適当なページにアクセスしてみましょう。特に変更を加えていないかぎり、リクエストヘッダーには、下記の項目が含まれています。

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

この設定は「ブラウザキャッシュは保持するが、常にサーバー確認を必須とする」ことを示しています。Cache-Control の設定を変えたい場合は expires_inno_store メソッドがあります。下記の記事で紹介されています。

https://qiita.com/jkr_2255/items/52706ad6bb0814e73fc9#シナリオ別の設定

Rails Guide ではこの項目については記載がないようです。もし、利用する場合は、ソースコードを読んだり、テストを行って、十分仕様を確認してから利用してください。なお、Cache-Control のデフォルト値の設定については下記のソースコードからも読み取れます。

https://github.com/rails/rails/blob/3769f1eee6a1ec4f6438155d67477db6d192577f/actionpack/lib/action_dispatch/http/cache.rb#L168

ETag と更新日時によるブラウザキャッシュの有効判定

Cache-Control リクエストヘッダーによりブラウザはレスポンスをキャッシュとして保存していることがわかりました。しかし、上記の設定のため、サーバーの検証を通過しないかぎり利用されません。サーバーの検証を行うには stale? メソッドを使います。stale? メソッドは、名前のシンプルさに反して、ややトリッキーです。ETag と更新日時のチェックを行い、ブラウザキャッシュが古いかどうかを調べます。そして、ブラウザキャッシュが古くない場合には false を返しつつレスポンスに 304 Not Modified をセットします。具体例は下記の通りです。

class HogeController
  def show
    @picture = Picture.find(params[:id])

    # stale? が false のとき自動でレスポンスを生成し 304 を返してくれるので何もしない
    return unless stale?(@picture)

    render_something(@picture)
  end
end

上記のコントローラは、1回目のリクエストに対して、下記のレスポンスヘッダーをセットします。

Etag: W/"4b5bb74b95869b1775c368b73349fd3c"   # これは @picture のハッシュ値(計算方法は後述)
Last-Modified: Tue, 16 Apr 2024 07:16:55 GMT # これは @pucture の updated_at

ブラウザは上記の内容を記憶します。そして、同じURLに対して2回目のアクセスを行う時には下記のリクエストヘッダーをセットします。

If-Modified-Since: Tue, 16 Apr 2024 07:16:55 GMT
If-None-Match: W/"4b5bb74b95869b1775c368b73349fd3c"

これを受け取ったコントローラは @picture の ETag と更新日時を調べます。もし ETag が一致し、 @picture の更新日時が古ければ、304 Not Modified を返します。そうでないなら、キャッシュを使わずにコントローラは通常通りレスポンスを生成します。この後 @picture が更新された場合 ETag が変化し、リクエスト時にブラウザキャッシュは利用されません。実際に Google Chrome でヘッダーを確認すると、期待通り動作していることがわかります。

ETag の計算方法

ETag はデフォルトでは下記の計算によって作られるようです。

def strong_etag(record)
  %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}")
end

ActiveSupport::Digest は設定次第で MD5, SHA1, SHA256 などが使われます。ソースコードは下記です。

https://github.com/rails/rails/blob/3769f1eee6a1ec4f6438155d67477db6d192577f/actionpack/test/controller/render_test.rb#L737-L739

Discussion