Railsのコレクションキャッシングでビューのレンダリングを高速化する方法
AppSignal Academyでは以前、Railsのフラグメントキャッシングについて取り上げました。フラグメントキャッシュはビューの小さな断片をキャッシュすることでパフォーマンスを大きく向上させます。さらに、パーシャルをキャッシュしておけば、同じパーシャルをビューの他の場所でもほぼコストなしで再利用できるというメリットもあります。
この手法は小さなコレクションでは十分に機能しますが、コレクションが大きくなるとすぐに問題が表面化します。本記事では、Railsのコレクションキャッシングの仕組みと、それを活用して大規模なコレクションのレンダリングを高速化する方法を解説します。
👋 この記事が気に入った方は、Ruby(on Rails)のパフォーマンスに関する記事も多数公開しています。ぜひ「Rubyパフォーマンス監視チェックリスト」もあわせてご覧ください。
コレクションのレンダリング
まずは、ブログのインデックスページ向けに最新100件の投稿を読み込むシンプルなコントローラーから始めましょう。
class PostsController < ApplicationController
def index
@posts = Post.all.order(:created_at => :desc).limit(100)
end
end
ビュー側では、@postsインスタンス変数をループ処理して投稿を表示します。
<!-- app/views/posts/index.html.erb -->
<h1>Posts</h1>
<div class="posts">
<% @posts.each do |post| %>
<div class="post">
<h2><%= post.title %></h2>
<small><%= post.author %></small>
<div class="body">
<%= post.body %>
</div>
</div>
<% end %>
</div>
このページにアクセスすると、データベースから投稿が取得され、ビューがレンダリングされるのがログから確認できます。ビューレイヤーの処理時間はわずか32ミリ秒で、かなりの高速さです。
Started GET "/posts"
Processing by PostsController#index as HTML
Rendering posts/index.html.erb within layouts/application
Post Load (1.5ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT ? [["LIMIT", 100]]
↳ app/views/posts/index.html.erb:4
Rendered posts/index.html.erb within layouts/application (19.4ms)
Completed 200 OK in 37ms (Views: 32.4ms | ActiveRecord: 2.7ms)
パーシャルを使ったコレクションのレンダリング
次に、post要素を別のビューでも使い回したいので、投稿のHTMLをパーシャルとして切り出します。
<!-- app/views/posts/index.html.erb -->
<h1>Posts</h1>
<div class="posts">
<% @posts.each do |post| %>
<%= render post %>
<% end %>
</div>
<!-- app/views/posts/_post.html.erb -->
<div class="post">
<h2><%= post.title %></h2>
<small><%= post.author %></small>
<div class="body">
<%= post.body %>
</div>
</div>
Started GET "/posts"
Processing by PostsController#index as HTML
Rendering posts/index.html.erb within layouts/application
Post Load (1.2ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT ? [["LIMIT", 100]]
↳ app/views/posts/index.html.erb:4
...
Rendered posts/_post.html.erb (0.1ms)
Rendered posts/_post.html.erb (0.1ms)
Rendered posts/index.html.erb within layouts/application (205.4ms)
Completed 200 OK in 217ms (Views: 213.8ms | ActiveRecord: 1.7ms)
ビューレイヤーに213ミリ秒もかかっており、レンダリング時間が大幅に増加したことがわかります。これは、投稿ごとに新しいファイル(パーシャル)を読み込み、コンパイルし、レンダリングする必要があるためです。続いて、フラグメントキャッシングでこのレンダリング時間を改善する方法を見てみましょう。
フラグメントキャッシング
フラグメントキャッシングの記事で説明したとおり、ビュー内のrender呼び出しをcacheヘルパーで囲みます。こうすることで、各投稿のパーシャルのレンダリング結果がキャッシュされます。
<!-- app/views/posts/index.html.erb -->
<h1>Posts</h1>
<div class="posts">
<% @posts.each do |post| %>
<%= cache post do %>
<%= render post %>
<% end %>
<% end %>
</div>
Started GET "/posts"
Processing by PostsController#index as HTML
Rendering posts/index_with_partial_caching.html.erb within layouts/application
Post Load (1.4ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT ? [["LIMIT", 100]]
↳ app/views/posts/index.html.erb:4
...
Read fragment views/posts/index.1ms)
Rendered posts/_post.html.erb (0.1ms)
Write fragment views/posts/index.1ms)
Read fragment views/posts/index.5ms)
Rendered posts/_post.html.erb (0.1ms)
Write fragment views/posts/index.1ms)
Rendered posts/index.html.erb within layouts/application (274.5ms)
Completed 200 OK in 286ms (Views: 281.4ms | ActiveRecord: 2.4ms)
最初のリクエストではあまり速くなりません。初回はすべてのパーシャルをレンダリングし、その結果をキャッシュストアに保存する必要があるためです。
Started GET "/posts"
Processing by PostsController#index as HTML
Rendering posts/index.html.erb within layouts/application
Post Load (2.2ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT ? [["LIMIT", 100]]
↳ app/views/posts/index.html.erb:4
...
Read fragment views/posts/index.1ms)
Read fragment views/posts/index.1ms)
Rendered posts/index.html.erb within layouts/application (63.8ms)
Completed 200 OK in 78ms (Views: 75.5ms | ActiveRecord: 2.2ms)
2回目以降のリクエストでは、ビューの処理時間が大幅に減少していることがわかります。286ミリ秒から78ミリ秒へと改善されました。とはいえ、それでも元のコードよりかなり遅く、ほぼ2倍の時間がかかっています。
補足:ログに「Read/Write fragment」の行が表示されない場合は、開発環境でフラグメントキャッシュのログ出力を有効にしてください。Rails 5.1以降ではデフォルトでfalseに設定されています。
# config/environments/development.rb
config.action_controller.enable_fragment_cache_logging = true
コレクションキャッシング
Rails 5では、コレクションキャッシングを高速化するために多くの改良が施されました。これらの改善を活用するには、ビューのコードを少し変更する必要があります。cacheヘルパーを自分で呼び出す代わりに、Railsに対して「コレクション全体のレンダリングと同時にキャッシュ」を依頼できます。
<!-- app/views/posts/index.html.erb -->
<h1>Posts</h1>
<div class="posts">
<%= render partial: :post, collection: @posts, cached: true %>
</div>
なお、render @collection, cached: trueという省略記法では、このキャッシングによる速度向上の恩恵を受けられない点に注意してください。
Started GET "/posts"
Processing by PostsController#index as HTML
Rendering posts/index.html.erb within layouts/application
Post Load (1.4ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT ? [["LIMIT", 100]]
↳ app/views/posts/index.html.erb:4
Rendered collection of posts/_post.html.erb [0 / 100 cache hits] (28.2ms)
Rendered posts/index.html.erb within layouts/application (46.6ms)
Completed 200 OK in 64ms (Views: 59.9ms | ActiveRecord: 2.0ms)
最初のリクエストの時点で、ビューレイヤーの処理時間に大きな改善が見られます。これは、Railsが投稿ごとに個別のパーシャルを準備するのではなく、コレクション全体で使用するパーシャルを事前に一括して準備するようになったためです。
Started GET "/posts"
Processing by PostsController#index as HTML
Rendering posts/index.html.erb within layouts/application
Post Load (1.3ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT ? [["LIMIT", 100]]
↳ app/views/posts/index.html.erb:4
Rendered collection of posts/_post.html.erb [100 / 100 cache hits] (19.2ms)
Rendered posts/index.html.erb within layouts/application (26.5ms)
Completed 200 OK in 37ms (Views: 35.7ms | ActiveRecord: 1.3ms)
2回目以降のリクエストでは、さらなる改善が見られます。64ミリ秒から約35ミリ秒へと短縮されました。これは、Railsがコレクション向けに最適化されているためです。各パーシャルのキャッシュ有無を一件ずつ確認する代わりに、コレクションの全キャッシュキーを一度にチェックすることで、キャッシュストアへの問い合わせ時間を節約しています。
このキャッシングヘルパーには、コレクションのログがサマリー形式で出力されるという追加のメリットもあります。最初のリクエストではキャッシュヒットがゼロ([0 / 100 cache hits])でしたが、2回目のリクエストではすべてヒット([100 / 100 cache hits])しました。
さらに、データベース内の一部のオブジェクトを更新すると、古くなった(無効化された)キーの数まで確認できます。
Rendered collection of posts/_post.html.erb [88 / 100 cache hits] (13.4ms)
このように最適化されたコレクションのレンダリングとキャッシングを組み合わせれば、大幅な速度向上が期待できます。コレクションが大きくなるほど、その差はさらに顕著になります。コレクションごとにカスタマイズされたビューが必要なケースを除けば、Railsアプリではこの最適化された戦略を採用するのがベストプラクティスです。実際にAppSignalでは、数千件のレコードをレンダリングしていた管理画面のビューを、この方法によって大幅に高速化することに成功しました。
Railsでのコレクションキャッシングについて疑問点はありますか?@AppSignalまでお気軽にお問い合わせください。記事へのフィードバックや、取り上げてほしいトピックのご提案もお待ちしています。
-
AWS LambdaでRailsアプリを動かす方法!Lambyを使ったデプロイ手順を徹底解説
サーバーレスコンピューティングは、サーバーの管理やプロビジョニングの作業をクラウドプロバイダーにオフロードできる仕組みとして、多くのテックチームで急速に標準になりつつあります。その代表格がAWS Lambdaです。LambdaはNode.js、Java、Python、Rubyといった主要なプログラミング言語を幅広くサポートしています。しかし、言語そのものだけでなく、それらの言語で構築されたフレームワークの機能に依存するサーバーレス関数を実行したいケースもあるでしょう。 本記事では、RailsアプリケーションをAWS Lambda上で実行する方法を解説します。サーバーレスコンピューティングとAW
-
Rails5でのAngularの使用
あなたは前にその話を聞いたことがあります。分散型で完全に機能するバックエンドAPIと、通常のツールセットで作成されたフロントエンドで実行されているアプリケーションがすでにあります。 次に、Angularに移動します。または、AngularをRailsプロジェクトと統合する方法を探しているだけかもしれません。これは、この方法を好むためです。私たちはあなたを責めません。 このようなアプローチを使用すると、両方の世界を活用して、たとえばRailsとAngularのどちらの機能を使用してフォーマットするかを決定できます。 構築するもの 心配する必要はありません。このチュートリアルは、この目的のた