Railsのクライアントサイドキャッシュ入門:条件付きGETリクエストとETagの活用法
ロシアン人形(Russian doll)キャッシュ以外にも、Railsアプリケーションのパフォーマンスを向上させる手法は数多くあります。本記事では、Railsに標準で組み込まれている「条件付きGETリクエスト(Conditional GET)」の仕組みを取り上げます。これを活用すれば、レンダリング済みのページをユーザーのブラウザキャッシュに保存できるようになります。
💡 キャッシュ以外のパフォーマンス改善についてさらに学びたい方は、Ruby(on Rails)のパフォーマンスに関する記事も多数公開しています。ぜひ「Rubyパフォーマンス監視チェックリスト」もあわせてご覧ください。
ETagヘッダーとLast-Modifiedヘッダー
ブラウザがRailsアプリケーションのページに対してHTTP GETリクエストを実行すると、ルーターがそのリクエストをコントローラのアクションに紐付けます。続いてコントローラが必要なデータをデータベースから取得し、ビューをレンダリングします。そして、レンダリングされたHTMLをレスポンスボディに含めたHTTPレスポンス(ステータスコードは200 OK)がブラウザへ返され、ブラウザはそれを解析して画面に表示するという流れです。
同じリソースが再度リクエストされると、通常は同じ処理パイプラインがもう一度実行されます。しかし、その間にページが変更されていないのであれば、この一連の処理は不要です。この課題を解決するために、HTTPにはETagとLast-Modifiedというヘッダーが用意されています。これらを利用すると、ブラウザはレスポンスボディを保存しておき、コンテンツが古くなったタイミングでヘッダー情報をもとにキャッシュを無効化できます。
ETag(Entityタグ)はクライアント側でのキャッシュ検証に使われるもので、「HTTPレスポンスのためのキャッシュキー」と考えると分かりやすいでしょう。すべてのリクエストに対して、HTTPレスポンスヘッダー経由でブラウザに渡されます。
~ $ curl -I https://localhost:3000/products/1
HTTP/1.1 200 OK
...
ETag: W/"9462d76cc55aeb6249fa990e39231c7c"
Last-Modified: Wed, 25 Apr 2018 08:27:04 GMT
...
その後、同じレスポンスが再び要求された場合、ブラウザは自身のキャッシュ内から既存のレスポンスを見つけ、前回のリクエストで保存しておいたETagをIf-None-Matchヘッダーとして送信します。このヘッダーによって、Railsアプリケーション側は「このバージョンをすでにキャッシュしている」ことを認識できます。
リクエストで送られてきたETagが現在のETagと一致していれば、Railsはレスポンスボディなしの304 Not Modifiedを返します。これを受け取ったブラウザは、ローカルキャッシュに保持していたコンテンツを代わりに使用します。
~ $ curl -i -H 'If-None-Match: W/"9462d76cc55aeb6249fa990e39231c7c"' https://localhost:3000/products/1
HTTP/1.1 304 Not Modified
...
ETag: W/"9462d76cc55aeb6249fa990e39231c7c"
Last-Modified: Wed, 25 Apr 2018 08:27:04 GMT
...
Railsにおける条件付きGETリクエストの挙動
ローカルで動作しているRailsアプリケーションにページをリクエストしてみると、Railsが各リクエストに対して自動的にETagを付与していることが分かります。ただし、同じページを連続して何度かリクエストすると、リクエストごとにETagの値が変化していることに気づくでしょう。
Railsはデフォルトで各リクエストにETagを生成しますが、その際にレスポンスボディ全体のダイジェストを使用しています。つまり、レイアウト内の<%= csrf_meta_tags %>がETagを狂わせてしまうのです。csrf-tokenのメタタグはリクエストごとに値が変わるため、レスポンスボディも毎回変化し、その結果ETagが無効化されてローカルキャッシュは「古い(stale)」状態として扱われます。
さらに、コントローラ内でローカルキャッシュを明示的に「新鮮(fresh)」としてマークしない限り、Railsはデフォルトでは304 Not Modifiedを一切返しません。
fresh_whenとstale?による実装
リクエストヘッダーのETagを条件付きGETに活用するには、ローカルキャッシュ内のオブジェクトを明示的に「新鮮(fresh)」としてマークする必要があります。例えば、ある製品を表示するページであれば、製品本体とビューテンプレートが変わらない限り、キャッシュを新鮮なまま保つことができます。これを実現するために、次の2つのことを行います。
- ETagを構成する値を明示的に指定します。レスポンスボディ全体を使う方式だと、キャッシュされたレスポンスが有効かどうかを確認するためにボディ全体をレンダリングし直す必要があり、ローカルキャッシュによる高速化のメリットが失われてしまうためです。
- ビューをレンダリングする前に予測したETagと、リクエストヘッダーから受け取ったETagを比較し、両者が一致していればレンダリング自体を省略します。
幸い、Railsにはこれらの処理をすべて自動的に行ってくれるヘルパーが用意されています。fresh_whenを使えば、ETagとLast-Modifiedの日時を製品オブジェクトに基づいて明示的に設定できます。
# app/controllers/products_controller.rb
def show
@product = Product.find(params[:id])
fresh_when @product
end
明示的なrespond_toブロックを使用している場合は、fresh_whenの代わりにstale?を使います。
# app/controllers/products_controller.rb
def show
@product = Product.find(params[:id])
if stale?(@product)
respond_to do |format|
format.html
end
end
end
これで、製品ページへのリクエスト時にレスポンスがローカルにキャッシュされるようになります。以降、同じページへのリクエストにはETagが含まれて送信され、Rails側はそれを新しく生成したETagと比較します。両者が一致すれば、Railsはページのレンダリングをスキップし、即座に304 Not Modifiedを返します。
補足: ページの再読み込み(更新)を行うと、常にキャッシュされていないバージョンがリクエストされます。条件付きGETが正しく動作しているか確認したい場合は、リンクで別ページへ移動したり、ブラウザの戻るボタンを使ったりしてテストしてください。
いかがでしたか?AppSignal Academyシリーズのこれまでの記事と合わせてお楽しみいただけたなら幸いです。今後もRailsにおけるキャッシュ関連の記事を予定しています。キャッシュ関連でもその他のトピックでも、「次にこんな記事を読みたい」というご要望があれば、ぜひお気軽にお知らせください!
-
AWS LambdaでRailsアプリを動かす方法!Lambyを使ったデプロイ手順を徹底解説
サーバーレスコンピューティングは、サーバーの管理やプロビジョニングの作業をクラウドプロバイダーにオフロードできる仕組みとして、多くのテックチームで急速に標準になりつつあります。その代表格がAWS Lambdaです。LambdaはNode.js、Java、Python、Rubyといった主要なプログラミング言語を幅広くサポートしています。しかし、言語そのものだけでなく、それらの言語で構築されたフレームワークの機能に依存するサーバーレス関数を実行したいケースもあるでしょう。 本記事では、RailsアプリケーションをAWS Lambda上で実行する方法を解説します。サーバーレスコンピューティングとAW
-
Rails5でのAngularの使用
あなたは前にその話を聞いたことがあります。分散型で完全に機能するバックエンドAPIと、通常のツールセットで作成されたフロントエンドで実行されているアプリケーションがすでにあります。 次に、Angularに移動します。または、AngularをRailsプロジェクトと統合する方法を探しているだけかもしれません。これは、この方法を好むためです。私たちはあなたを責めません。 このようなアプローチを使用すると、両方の世界を活用して、たとえばRailsとAngularのどちらの機能を使用してフォーマットするかを決定できます。 構築するもの 心配する必要はありません。このチュートリアルは、この目的のた