Chúng ta đã xem fragment caching trong Rails trên AppSignal Academy. Điều này cải thiện performance views lớn bằng cách cache các mảnh nhỏ. Khi cache partials, có thêm benefit reuse chúng elsewhere trong views với chi phí thấp.
Cách này tốt cho collection nhỏ, nhưng vấn đề nhanh chóng nảy sinh trên collection lớn. Bài này xem Rails collection caching hoạt động như thế nào và dùng nó speed up rendering large collection.
👋 Nếu thích bài này, chúng tôi viết nhiều về Ruby (on Rails) performance, check Ruby performance monitoring checklist.
Rendering Collection
Bắt đầu với controller nhỏ load 100 bài post mới nhất cho blog index page.
class PostsController < ApplicationController
def index
@posts = Post.all.order(:created_at => :desc).limit(100)
end
end
Render posts trong view, loop qua @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>
Request page, thấy posts fetched từ database và view rendered. Chỉ 32ms spent view layer, page khá nhanh.
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)
Rendering Collection với Partials
Muốn dùng element post ở view khác, move post HTML sang partial.
<!-- 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)
Với 213ms view layer, render time tăng substantially. Vì file mới (partial) cần load, compile, render cho mỗi post. Hãy xem fragment caching cải thiện như thế nào.
Fragment Caching
Như fragment caching article, dùng helper cache trong view quanh render. Cách này cache rendering partial cho mỗi post.
<!-- 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)
Request đầu không nhanh hơn nhiều, vì vẫn render mọi partial lần đầu và store cache.
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)
Request sau, view time thấp hơn đáng kể - từ 286ms xuống 78ms. Vẫn chậm gấp đôi code gốc.
Note: Nếu không thấy dòng "Read/Write fragment" trong log, bật fragment cache logging development env (mặc định false Rails 5.1+):
# config/environments/development.rb
config.action_controller.enable_fragment_cache_logging = true
Collection Caching
Rails 5 cải thiện collection caching nhanh hơn. Để tận dụng, thay đổi view code. Thay vì tự gọi cache helper, nhờ Rails render toàn bộ collection và cache cùng lúc.
<!-- app/views/posts/index.html.erb -->
<h1>Posts</h1>
<div class="posts">
<%= render partial: :post, collection: @posts, cached: true %>
</div>
Lưu ý shorthand render @collection, cached: true không work cho caching speed improvement này.
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)
Request đầu đã thấy improvement lớn view layer. Vì Rails prepare trước partial cho toàn bộ collection, thay vì từng post riêng.
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)
Request sau improvement hơn - từ 64ms xuống ~35ms. Speed improvement lớn cho entire collection bởi Rails optimize cho collections. Thay vì check cache availability cho mỗi partial, Rails check tất cả cache keys collection cùng lúc, tiết kiệm time query cache store.
Benefit thêm: logging summarized cho collection. Request đầu không cache key found [0 / 100 cache hits], request 2 tất cả found [100 / 100 cache hits].
Sau update một số object database, thấy bao nhiêu key stale.
Rendered collection of posts/_post.html.erb [88 / 100 cache hits] (13.4ms)
Nhiều speed improvement với optimized collection rendering/caching. Difference lớn hơn với collection lớn. Trừ khi cần customized views cho collection, optimized strategy này là cách đi cho Rails apps. Tại AppSignal, chúng tôi significantly speed up admin view render thousands records cách này.
Có câu hỏi caching collections Rails? Đừng ngần ngại @AppSignal! Comment hoặc topic muốn cover, get in touch.