Tìm hiểu Fragment Caching trên Rails 4.2

1.Giới thiệu

Fragment Caching là cơ chế bộ nhớ đệm được nhiều lập trình viên sử dụng nhất. Những dòng code giống nhau sẽ trả ra kết quả 1 cách nhanh chóng hơn. Nhưng mặt khác nó cũng rất khó để debug cũng như nếu không sử dụng hợp lý sẽ dẫn đến quá tải resource. Qua bài viết này tôi muốn hướng dẫn các bạn làm sao để sử dụng Fragment Caching 1 cách hiệu quả nhất.

2.Thiết lập

2.1 Thiết lập dữ liệu

Để chứng minh hiệu quả của Fragment Catching trước hết chúng ta phải tạo 1 lượng dữ liệu đủ lớn để dễ dàng so sánh thời gian chạy của chương trình. Để làm được điều đó ở đây tôi dùng Faker Gem để tạo 1000 người và 100 công ty với điều kiện mỗi người đều làm việc ở 1 công ty.

app/models/company.rb

class Company < ActiveRecord::Base
  has_many :people
  def to_s
    name
  end
end

db/seeds.rb

100.times {Company.create(name: Faker::Company.name)}
1000.times {Person.create(first_name: Faker::Name.first_name,
  last_name: Faker::Name.last_name, company: Company.all.sample)}

2.2 Activate Cache trong môi trường development

Mặc định Caching bị tắt trong môi trường development. Bạn phải setting active chức năng này ở file config/environments/development.rb

[...]
config.action_controller.perform_caching = true
[...]

2.3 Phương thức hoạt động của Fragment Cache

Trong một vài trường hợp trình duyệt sẽ mất 1 khoảng thời gian tương đối để render 1 file HTML từ hệ thống. Ví dụ: app/views/page/index.html.erb

<h1>Loop Example</h1>
<ul>
  <% 3.times do %>
    <% sleep 1 %>
    <li>test</li>
  <% end %>
</ul>

Ở ví dụ trên vì mỗi 1 vòng lặp ta để dừng lại 1s nên khi load sẽ rất chậm. Rails mất đến 3.080 ms để load trang

Started GET "/" for ::1 at 2016-05-24 13:48:00 +0100
Processing by PageController#index as HTML
  Rendered page/index.html.erb within layouts/application (3003.9ms)
Completed 200 OK in 3080ms (Views: 3079.4ms | ActiveRecord: 0.0ms)

Vẫn với đoạn HTML trên nhưng khi ta sử dụng Fragment Caching app/views/page/index.html.erb

<h1>Loop Example</h1>
<% cache do %>
  <ul>
    <% 3.times do %>
      <% sleep 1 %>
      <li>test</li>
    <% end %>
  </ul>
<% end %>

Ở lần thực thi đầu tiên tốc độ sẽ không được cải thiện vì lúc này cache của đoạn HTML trên mới được lưu lại. Nhưng ở lần chạy thứ 2 chúng ta có thể thấy được sự khác biệt rõ rệt. Tốc độ load trang chỉ còn 74ms

Started GET "/" for ::1 at 2016-05-24 13:53:44 +0100
Processing by PageController#index as HTML
  Cache digest for app/views/page/index.html.erb: 923da8c3db3a930ad66428f858e00cce
Read fragment views/localhost:3000/page/index/923da8c3db3a930ad66428f858e00cce (0.2ms)
  Rendered page/index.html.erb within layouts/application (1.5ms)
Completed 200 OK in 74ms (Views: 73.1ms | ActiveRecord: 0.0ms)

Rails tạo ra một cache key từ MD5 của file (trong trường hợp này là file app/views/page/index.html.erb). Vì vậy khi nội dung của file thay đổi thì cache sẽ bị expire. Ngoài ra để xóa bộ nhớ đệm trong môi trường development bạn có thể sử dụng lệnh:

rake tmp:cache:clear

Fragment Caching thường được sử dụng kết hợp với ActiveRecord.

Quay lại với chương trình ban đầu của chúng ta với dữ liệu là 1000 người và 100 công ty. Khi ta muốn show thông tin của 1 người lên ta phải mất đến 121ms:

Started GET "/people/1" for ::1 at 2016-05-24 14:16:53 +0100
Processing by PeopleController#show as HTML
  Parameters: {"id"=>"1"}
  Person Load (0.1ms)  SELECT  "people".* FROM "people" WHERE "people"."id" = ? LIMIT 1  [["id", 1]]
  Company Load (0.1ms)  SELECT  "companies".* FROM "companies" WHERE "companies"."id" = ? LIMIT 1  [["id", 69]]
  Rendered people/show.html.erb within layouts/application (1.0ms)
Completed 200 OK in 121ms (Views: 119.9ms | ActiveRecord: 0.2ms)

Thử đoán xem với Fragment Caching như bên dưới ta sẽ có kết quả như thế nào? app/views/people/show.html.erb

<p id="notice"><%= notice %></p>
<% cache @person do %>
  <p>
    <strong>First name:</strong>
    <%= @person.first_name %>
  </p>
  <p>
    <strong>Last name:</strong>
    <%= @person.last_name %>
  </p>
  <p>
    <strong>Company:</strong>
    <%= @person.company %>
  </p>
  <%= link_to 'Edit', edit_person_path(@person) %> |
  <%= link_to 'Back', people_path %>
<% end %>

Sau khi fragment cache được lưu lại sau lần tải trang đầu tiên thì ta có kết quả load trang là 91ms

Started GET "/people/1" for ::1 at 2016-05-24 14:24:01 +0100
Processing by PeopleController#show as HTML
  Parameters: {"id"=>"1"}
  Person Load (0.1ms)  SELECT  "people".* FROM "people" WHERE "people"."id" = ? LIMIT 1  [["id", 1]]
  Cache digest for app/views/people/show.html.erb: 512ccb993dfd163db3a3799b571bf196
Read fragment views/people/1-20150223194338611556000/512ccb993dfd163db3a3799b571bf196 (0.2ms)
  Rendered people/show.html.erb within layouts/application (1.3ms)
Completed 200 OK in 91ms (Views: 90.0ms | ActiveRecord: 0.1ms)

Trong trường hợp này fragment cache vẫn được lưu lại nhưng có 1 chút phức tạp hơn. Ngoài cache cho tập tin, thêm vào đó sẽ thêm 1 cache cho các đối tượng ở đây là person object. Bạn có thể kiểm tra lại ở trong console:

$ rails c
Loading development environment (Rails 4.2.0)
>> Person.first.cache_key
  Person Load (0.1ms)  SELECT  "people".* FROM "people"  ORDER BY "people"."id" ASC LIMIT 1
=> "people/1-20150223194338611556000"

Vì phương thức cache_key thực thi mỗi khi update object trong database nên ta có thể dễ dàng lợi dụng để cache view khi view đó sử dụng ActiveRecord.

Tiếp đến đối với file app/views/people/index.html.erb

<p id="notice"><%= notice %></p>
<% cache @people do %>
<h1>Listing People</h1>
<table>
  <thead>
    <tr>
      <th>First name</th>
      <th>Last name</th>
      <th>Company</th>
      <th colspan="3"></th>
    </tr>
  </thead>
  <tbody>
    <% @people.each do |person| %>
      <tr>
        <td><%= person.first_name %></td>
        <td><%= person.last_name %></td>
        <td><%= person.company %></td>
        <td><%= link_to "Show", person %></td>
        <td><%= link_to "Edit", edit_person_path(person) %></td>
        <td><%= link_to "Destroy", person, method: :delete, data: {confirm: "Are you sure?"} %></td>
      </tr>
    <% end %>
  </tbody>
</table>
<br>
<%= link_to "New Person", new_person_path %>
<% end %>

Trong trường hợp này chúng ta có vẻ đã tốn khá nhiều thời gian cho việc tạo 1 cache_key cho mỗi object person trong biến @people. Việc này sẽ thực thi lặp đi lặp lại mỗi khi cache này được sử dụng, điều này là không cần thiết. Thay vì thế ta có thể thực hiện chỉ tạo 1 cache_key cho view này và expire cache 1 cách tự động. Trước tiên ta tao 1 method ở trong file app/helpers/people_helper.rb để tạo 1 cache_key

module PeopleHelper
  def cache_key_for_people
    count          = Person.count
    max_updated_at = Person.maximum(:updated_at).try(:utc).try(:to_s, :number)
    "people/all-#{count}-#{max_updated_at}"
  end
end

Sửa lại file app/views/people/index.html.erb

<% cache cache_key_for_people do %>
  <h1>Listing People</h1>
  <table>
  [...]
  </table>
  <br>
  <%= link_to 'New Person', new_person_path %>
<% end %>

Cứ mỗi khi 1 person trong biến @people được update hoặc xóa đi thì fragment cache sẽ được expire một cách tự động và tạo ra 1 cache mới. Cuối cùng ta có được 1 cache_key nhỏ hơn và cần ít thời gian hơn để load trang

Started GET "/people/" for ::1 at 2015-02-24 15:13:46 +0100
Processing by PeopleController#index as HTML
   (0.2ms)  SELECT COUNT(*) FROM "people"
   (0.4ms)  SELECT MAX("people"."updated_at") FROM "people"
  Cache digest for app/views/people/index.html.erb: 71351c5b261729771b8454e4e59344bd
Read fragment views/people/all-1000-20150223194338/71351c5b261729771b8454e4e59344bd (0.4ms)
  Rendered people/index.html.erb within layouts/application (4.0ms)
Completed 200 OK in 116ms (Views: 115.0ms | ActiveRecord: 0.6ms)

3.Kết luận

Trên đây là hướng dẫn cơ bản về việc sử dụng Fragment Caching trong rails nhằm cải thiện thời gian load trang. Bạn có thể đọc thêm chi tiết về Fragment Caching tại http://guides.rubyonrails.org/caching_with_rails.html#fragment-caching. Chúc các bạn thành công !!!