Khi ứng dụng Rails nhận request, controller thường hỏi model để lấy data. Model fetch từ database và trả về controller. Controller cuối cùng render view thể hiện data dạng human-readable.
Trong một số tình huống, render view là operation tốn kém, đặc biệt nếu view cần hiển thị nhiều data, như danh sách tất cả sản phẩm trong store. Trong trường hợp đó, cache phần của returned view có thể tăng tốc, nhất là khi data không thay đổi thường xuyên.
👋 Nếu thích bài này, xem thêm về hiệu suất Ruby (on Rails) tại checklist giám sát hiệu suất Ruby của chúng tôi.
Test Caching Local
Caching tắt trong development mặc định, để đảm bảo luôn nhận response fresh từ app. Để test caching local, phải bật trong development config.
Trong Rails 5, có thể tạm bật caching từ command line. Sẽ dùng memory store, nghĩa là cached fragment giữ trong bộ nhớ process Ruby của web server.
$ rails dev:cache Development mode is now being cached. Chạy cùng lệnh để tắt caching lại.
Fragment Caching
Giả sử ta có trang hiển thị tất cả sản phẩm trong store trên một trang. Để làm vậy, ta có index view hiển thị sản phẩm.
# app/views/products/index.html.erb <table> <thead> <tr> <th>Title</th> <th>Description</th> <th>Image url</th> <th>Price</th> <th colspan="3"></th> </tr> </thead> <tbody> <% @products.each do |product| %> <%= render product %> <% end %> </tbody> </table>Với mỗi product, partial _product.html.erb được render, hiển thị table row với chi tiết sản phẩm.
# app/views/products/_product.html.erb <tr> <td><%= product.title %></td> <td><%= product.description %></td> <td><%= product.image_url %></td> <td><%= product.price %></td> <td><%= link_to 'Show', product %></td> <td><%= link_to 'Edit', edit_product_path(product) %></td> <td><%= link_to 'Destroy', product, method: :delete, data: { confirm: 'Are you sure?' } %></td> </tr>Request trang local, development mode, với 25 sản phẩm trong database mất ~300ms. Gần như toàn bộ thời gian render partials.
Có thể response nhanh hơn production, do bundled assets, ít logging hơn và web server nhanh hơn. Mặc dù số liệu development không chính xác, chúng chỉ ra phần request chậm nhất, để ta tối ưu.
Started GET "/products" for ::1 at 2018-03-13 12:16:08 +0100 Processing by ProductsController#index as HTML Rendering products/index.html.erb within layouts/application Product Load (0.4ms) SELECT "products".* FROM "products" Rendered products/_product.html.erb (1.4ms) Rendered products/_product.html.erb (0.4ms) Rendered products/_product.html.erb (0.4ms) Rendered products/_product.html.erb (0.3ms) Rendered products/_product.html.erb (0.5ms) Rendered products/_product.html.erb (2.0ms) Rendered products/_product.html.erb (0.9ms) Rendered products/_product.html.erb (0.4ms) Rendered products/_product.html.erb (0.5ms) Rendered products/_product.html.erb (0.5ms) Rendered products/_product.html.erb (0.6ms) Rendered products/_product.html.erb (0.6ms) Rendered products/_product.html.erb (0.6ms) Rendered products/_product.html.erb (0.6ms) Rendered products/_product.html.erb (0.7ms) Rendered products/_product.html.erb (0.6ms) Rendered products/_product.html.erb (0.7ms) Rendered products/_product.html.erb (0.6ms) Rendered products/_product.html.erb (0.5ms) Rendered products/_product.html.erb (0.7ms) Rendered products/_product.html.erb (0.6ms) Rendered products/_product.html.erb (0.9ms) Rendered products/_product.html.erb (0.6ms) Rendered products/_product.html.erb (0.5ms) Rendered products/_product.html.erb (0.6ms) Rendered products/index.html.erb within layouts/application (257.5ms) Completed 200 OK in 295ms(Views: 290.4ms | ActiveRecord: 0.4ms) Để tiết kiệm thời gian render tất cả partials, ta dùng fragment caching built-in của Rails, lưu phần rendered view làm fragment. Cho request tiếp theo, pre-saved fragment dùng thay vì render lại.
Để cache fragment, bọc nó trong block dùng helper cache.
<table> # ... <tbody> <% @products.each do |product| %> <% cache(product) do %> <%= render product %> <% end %> <% end %> </tbody> </table>Để xem cache sản phẩm có tăng tốc response không, ta request trang 2 lần. Request thứ 2 nên nhanh hơn nhiều, vì mỗi sản phẩm trong view đã pre-render và lưu trong cache.
Started GET "/products" for ::1 at 2018-03-13 12:17:29 +0100 Processing by ProductsController#index as HTML Rendering products/index.html.erb within layouts/application Product Load (0.4ms) SELECT "products".* FROM "products" Rendered products/index.html.erb within layouts/application (21.2ms) Completed 200 OK in 55ms (Views: 50.8ms | ActiveRecord: 0.4ms) Hoạt động! Request thứ 2 nhanh hơn gấp 5 lần request đầu. Lưu ý log không show render partials, vì load trực tiếp từ cache.
Hết Hạn Cache (Expiring the Cache)
Khi gọi helper cache ví dụ trên, ta truyền object product làm cache dependency. Điều này cho helper biết nội dung cached fragment phụ thuộc vào product object.
Bên trong, product object có phương thức #cache_key, dùng build key cho cached fragment. Key cho toàn bộ fragment có thể trông như:
views/products/42-20180302103130041320/75dda06d36880e8b0ae6cac0a44fb56d Cache key gồm vài phần:
- "views/products" là cache class.
42là product ID20180302103130041320là updated_at của product75dda06d36880e8b0ae6cac0a44fb56dlà digest của template tree
Key thay đổi khi product update, hoặc bất cứ gì trong template thay đổi. Khi đó, gây cache miss, fragment render lại và lưu fragment mới. Đảm bảo fragment fresh, ngay khi component thay đổi.
Vượt Quá Fragment Caching
Mặc dù cached fragment trong ví dụ cho speedup nhỏ, caching còn nhiều hơn chúng ta bàn hôm nay. Bằng strategies như Russian doll caching hoặc lower-level methods như cache database query results, speedup lớn hơn có thể đạt được.
Chắc chắn chúng tôi sẽ đi sâu vào những này trong các episode sau của AppSignal Academy. Có gì cụ thể bạn muốn học? Đừng ngần ngại liên hệ @AppSignal. Chúng tôi rất muốn biết bạn thấy bài này như thế nào, hoặc chủ đề gì bạn muốn biết thêm.