Railsアプリのパフォーマンス向上:条件付きGETリクエストでクライアント側キャッシュを使いこなす
Russian dollキャッシング以外にも、Railsアプリのパフォーマンスを高速化するテクニックは数多くあります。今回は、レンダリング済みのページをユーザーのブラウザキャッシュに保存できる、Rails組み込みの「条件付きGET(Conditional GET)」サポートについて詳しく見ていきましょう。
👋 なお、キャッシュ以外のパフォーマンスに関する記事も多数執筆しています。Ruby(on Rails)のパフォーマンス監視チェックリストもあわせてぜひご覧ください。
EtagヘッダーとLast-Modifiedヘッダー
ブラウザがRailsアプリのページに対してHTTP GETリクエストを実行すると、ルーターはそのリクエストをコントローラのアクションに紐付けます。コントローラはデータベースから必要なデータを取得し、ビューをレンダリングします。その後、レスポンスコード200 OKとともに、レンダリングされたHTMLがレスポンスボディに格納されてブラウザへ返され、ブラウザはそれを解析して画面に表示します。
同じリソースが再度リクエストされると、通常は同じパイプラインがもう一度実行されます。しかし、ページがその間に変更されていないのであれば、この一連の処理は不要です。そこで活躍するのが、HTTPが標準で提供するETagとLast-Modifiedヘッダーです。これらを利用することで、ブラウザはレスポンスボディを保存しておき、ヘッダー情報をもとに古くなった(staleな)キャッシュを無効化できるようになります。
ETag(Entity Tag)はクライアント側のキャッシュ検証に使われる仕組みで、HTTPレスポンスにおける「キャッシュキー」と考えると分かりやすいでしょう。ETagはすべてのリクエストに対して、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(古い)と判定されてしまいます。
さらに、Railsはデフォルトでは304 Not Modifiedを一切返しません。コントローラ内でローカルキャッシュが明示的にfresh(新しい)とマークされない限り、キャッシュの有効性は判断できないためです。
fresh_whenとstale?
リクエストヘッダーのETagを条件付きGETに活用するには、ローカルキャッシュ内のオブジェクトを明示的に「fresh」とマークする必要があります。たとえば、ある製品を表示するページであれば、製品とビューテンプレートが変更されない限りキャッシュをfreshのまま保てます。そのために行うことは、次の2つです。
- ETagを構成する値を明示的に指定する。レスポンスボディ全体をベースにすると、キャッシュされたレスポンスが有効かどうかを確認するためにボディ全体をレンダリングする必要があり、それではローカルキャッシュによる高速化の意味がなくなってしまいます。
- リクエストヘッダーのETagと、ビューをレンダリングする前に予測したETagを比較し、一致していればレンダリング自体を省略する。
幸い、Railsにはこれらすべてを自動的に処理してくれるヘルパーが用意されています。fresh_whenを使えば、ETagとLast-Modifiedの日付を製品オブジェクトに基づいて明示的に設定できます。
class ProductsController < ApplicationController
def show
@product = Product.find(params[:id])
fresh_when @product
end
end
respond_toブロックを明示的に記述している場合は、fresh_whenの代わりにstale?を使用します。
def show
@product = Product.find(params[:id])
respond_to do |format|
if stale? @product
format.html # show.html.erb をレンダリング
format.json { render json: @product }
end
end
end
これで、製品ページへの最初のリクエスト時にはレスポンスがローカルにキャッシュされます。以降の同一ページへのリクエストにはETagが含まれ、「キャッシュ済みのレスポンスを持っている」ことをRailsに伝えます。Railsはこれを新しく計算したETagと比較し、両者が一致すればページのレンダリングをスキップして、即座に304 Not Modifiedを返します。

注意: ページの更新(リロード)を行うと、常にキャッシュされていない最新のバージョンがリクエストされます。条件付きGETが正しく機能しているかを確認したい場合は、リンクで別ページへ移動してから戻るボタンを使うなどしてアクセスしてください。
この記事やAppSignal Academyシリーズの他の記事はいかがでしたか? 今後もRailsのキャッシュに関する記事を用意していますが、次に読みたいテーマ(キャッシュ関連でもその他でも)があれば、ぜひお気軽にお知らせください!
Jeff Kreeftmeijer
-
Unicornはどのようにnginxと通信するのか?Rubyで学ぶUNIXソケットの基礎
Ruby製のアプリケーションサーバーは、通常nginxのようなWebサーバーと組み合わせて運用されます。ユーザーがRailsアプリケーションのページをリクエストすると、nginxがそのリクエストを受け取り、アプリケーションサーバーへ処理を委譲します。しかし、この仕組みは具体的にどのように実現されているのでしょうか?nginxはどのようにしてUnicornと会話しているのでしょうか? 最も効率的な方法のひとつが「UNIXドメインソケット(UNIXソケット)」を使うことです。本記事では、まずソケットの基本から解説し、最終的には自作のシンプルなアプリケーションサーバーを作成して、それをnginxでプ
-
Ruby on Rails学習におすすめの必読書ガイド|レベル別に厳選
Webアプリケーション開発やデザインの分野で働いている方なら、「Ruby on Rails」という名前を一度は耳にしたことがあるでしょう。Rubyは長年の実績を持つ確立されたプログラミング言語であり、そこにRails(Rubyの機能を拡張するソフトウェアライブラリ)を組み合わせることで、強力なモダンアプリケーション開発ツールが手に入ります。Ruby on Railsを学べる書籍は数多く出版されていますが、どれが自分に合っているのか迷ってしまう方も多いのではないでしょうか。 この記事では、あなたの学習スタイルや目的にぴったりの1冊を見つけるためのガイドをご紹介します。プログラミングやアプリ開発の