← Quay lại
04 tháng 01, 2026 7 phút đọc

Thử Fragment Caching của Rails

There are only two hard things in Computer Science: cache invalidation and naming things.
—Phil Karlton

Trong môn Khoa học Máy tính chỉ có hai việc là khó hơn hết thảy đó là: làm mất hiệu lực cache và đặt tên.
—Nukun (Dịch câu trên ;))

Hôm nay chúng ta sẽ làm điều khó nhất bằng việc cache trang home của blog này (nhưng nhờ sự trợ giúp của Rails, mà đúng ra là Rails nó làm hết rồi mình chỉ gõ lệnh thôi 😋). Trang home hiện danh sách các bài viết đã publish. Mỗi lần trình duyệt yêu cầu trang này, controller sẽ gọi database lấy danh sách bài viết rồi nhờ View tạo HTML cho chúng bằng lệnh render:

<%= render partial: 'post', collection: @posts %>

Partial _post.html.erb nhận một biến post đại diện cho từng record của @posts để tạo ra HTML card như hình dưới:

Card giới thiệu bài viết


Đây là log thời gian request trang home:

Rendered home/index.html.erb within layouts/application (Duration: 120.5ms | GC: 18.4ms)
...
Completed 200 OK in 126ms (Views: 107.2ms | ActiveRecord: 16.0ms (39 queries, 0 cached) | GC: 18.4ms)

Giờ mình sẽ cache trang này lại xem thử có cải thiện thời gian tải trang không nhé. Blog mình dùng Rails 8.1. Dưới local, controller mặc định không dùng cache. Nên trước hết mình phải bật tính năng cache này (lưu ý là production đã được bật sẵn):

bin/rails dev:cache

Ở đây mình dùng Fragment Caching để cache view logic, có nghĩa là mỗi lần tải trang không cần phải render lại từng card mà chỉ làm điều đó lần đầu rồi cache lại để dùng cho lần sau. Cách làm rất đơn giản, bọc logic view vào một cache block như này:

<% @posts.each do |post| %>
  <% cache post do %>
    <%= render post %>
  <% end %>
<% end %>

Nhưng ta đang dùng render partial với collection, Rails có cách nào dùng trong trường hợp này không? Tất nhiên là có, đó là thêm option cached: true

<%= render partial: 'post', collection: @posts, cached: true %>

Cùng kiểm tra xem nhé! Lần request vẫn như cũ, server load và render như bình thường. Log có thêm dòng này

Rendered collection of home/_post.html.erb [0 / 9 cache hits] (Duration: 84.0ms | GC: 9.9ms)

thể hiện thông tin cache. Số 0 / 9 nghĩa là có 9 card nhưng chưa có card nào được cache (nhưng server đã lưu HTML vào cache để dùng cho lần sau). Giờ tải lại trang và xem log:

Rendered collection of home/_post.html.erb [9 / 9 cache hits] (Duration: 1.4ms | GC: 0.3ms)
...
Completed 200 OK in 12ms (Views: 7.4ms | ActiveRecord: 2.0ms (1 query, 0 cached) | GC: 0.3ms)

Yeah, 9 / 9 cache hits có nghĩa là 9 card được cache trước đó đã được dùng tới. So sánh log với khi chưa dùng cache cho dòng Completed... Trước đó cần 126ms để server hoàn thành request nhưng giờ chỉ còn 12ms, nhanh gấp 10 lần! Thật tuyệt đúng không nào? Thế... khi nào thì cache này mất tác dụng? Nếu mình đăng bài mới, xoá bài cũ hay sửa bài đã publish thì sao? Hãy cùng thử từng trường hợp nhé.

Đầu tiên là xoá một bài. Log hiện:

Rendered collection of home/_post.html.erb [8 / 8 cache hits] (Duration: 2.7ms | GC: 1.0ms)

OK, trường hợp này thì đơn giản, xoá thì mất cache, vậy thôi. Tiếp, khi sửa một bài viết đã publish rồi tải lại trang. Trang phản ánh nội dung đã thay đổi kèm log:

Rendered collection of home/_post.html.erb [7 / 8 cache hits] (Duration: 13.3ms | GC: 0.0ms)

Chỉ có 7 cache của card được dùng tới, vì card của post có nội dung thay đổi nên không dùng được cache đó nữa hay nói cách khác cache đó đã bị vô hiệu và bạn nhận ra điều gì không? Đó là điều khó nhất mà ngài Phil Karlton đã nói, chúng mình vừa làm được điều khó nhất trong lập trình! Nhưng tại sao? Trước khi tìm hiểu nguyên nhân hay cơ chế mà Rails thiết lập cho Fragment Caching ta hãy đi nốt trường hợp còn lại là thêm một bài mới. Hẳn nhiên ta sẽ đoán được là log hiện 8 / 9 cache hits đúng không nào và chính cmn xác:

Rendered collection of home/_post.html.erb [8 / 9 cache hits] (Duration: 11.3ms | GC: 0.0ms)

Đã tới lúc tìm câu trả lời cho câu hỏi trên. Rails làm điều này như thế nào?

Mỗi cache gắn với một key, Rails dùng nó để truy xuất cache. Để tìm hiểu cách tạo key chúng ta hãy xét riêng cho một post và dùng key của post đó làm ví dụ xuyên suốt. Code cache cho một post là:

<% post = @posts.first %>
<% cache post do %>
  <%= render partial: 'post', locals: { post: post } %>
<% end %>

Khi tải trang sẽ thấy log hiện:

Read fragment views/home/index:8fe62c17b3d769a887fc8b73dd7a12b8/posts/39-20260104135913946778 (0.3ms)
...
Write fragment views/home/index:8fe62c17b3d769a887fc8b73dd7a12b8/posts/39-20260104135913946778 (0.2ms)

Tải lại một lần nữa và xem log:

Read fragment views/home/index:8fe62c17b3d769a887fc8b73dd7a12b8/posts/39-20260104135913946778 (0.3ms)

Lần này không có dòng write. Điều này có nghĩa là lần đầu tiên Rails sẽ tìm cache của post theo key views/home/index:8fe62c17b3d769a887fc8b73dd7a12b8/posts/39-20260104135913946778 nhưng không thấy nên nó sẽ render HTML và viết cache theo key (dòng write). Lần sau khi tải lại, Rails dựa vào key của post tìm cache, nếu có thì hiện ra. Thử sửa lại thông tin post để xem key sẽ thay đổi ra sao nhé:

Read fragment views/home/index:8fe62c17b3d769a887fc8b73dd7a12b8/posts/39-20260104140411451755 (0.2ms)

Key giờ là views/home/index:8fe62c17b3d769a887fc8b73dd7a12b8/posts/39-20260104140411451755. Vậy key cấu thành từ gì?
  • views/home/index là nơi mình gọi code cache.
  • 8fe62c17b3d769a887fc8b73dd7a12b8 là digest của view này mà Rails tự động tạo (bằng cách nào thì mình chưa rõ).
  • posts/39-20260104135913946778 chính là key của post dùng để lưu cache. Nó tạo từ method post.cache_key_with_version. Bạn có thể gọi hàm này trong rails console và sẽ thấy kết quả y chang.
  • 39 là id, 20260104135913946778 là timestamp của updated_at.

Khi ta thay đổi thông tin post → thay đổi updated_at → thay đổi fragment cache key → cache mất hiệu lực. Lần tới khi tải trang sẽ tạo lại cache mới. Ngon lành! Lúc này bạn có thể đặt câu hỏi nếu thay đổi thông tin post nhưng không chạm tới updated_at thì sao? Đây sẽ là bài tập cho các bạn. Tự suy nghĩ câu trả lời nha :))

Có lẽ khá là dài rồi phải không. Rails còn rất nhiều thứ liên quan tới cache như Russian Doll Caching, Low-level Caching, Custom cache key... Rồi còn lưu cache ở đâu, trên memory, disk hay database,... Nhưng hãy để dành nó cho những bài viết sau nhé (hoặc các bạn cứ thoải mái tìm hiểu và áp dụng cho bản thân).

Vậy là ta đã làm được điều mà người tài giỏi hơn đánh giá là khó nhất trong khoa học máy tính. Tất nhiên là có sự giúp đỡ (rất lớn) từ Rails. Nhưng ta vẫn có quyền tự hào vì đã học và làm được điều đó. Giờ trang home của mình đã tải nhanh hơn gấp 10 lần trước. Nếu có thể bạn hãy thử và chia sẻ kinh nghiệm của mình nhé!

Còn giờ nên làm gì tiếp nhỉ? Có lẽ là... đặt tên. (Tham lam quá!)
Cảm ơn bạn đã đọc bài viết này.
18