Fragment caching allows you to store parts of your web pages in memory, so the next time that part is needed, it can be served directly from the cache instead of being regenerated by your application.
Let’s see it in action with a simple example. Imagine a product page that displays product details, a list of reviews, and a "related products" carousel.
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def show
@product = Product.find(params[:id])
@reviews = @product.reviews.order(created_at: :desc)
@related_products = @product.related_products.limit(5)
end
end
<%# app/views/products/show.html.erb %>
<h1><%= @product.name %></h1>
<div id="product-details">
<%= render 'details', product: @product %>
</div>
<div id="reviews">
<h2>Customer Reviews</h2>
<%= render @reviews %>
</div>
<div id="related-products">
<h2>Related Products</h2>
<%= render @related_products %>
</div>
The show action fetches the product, its reviews, and related products. The view then renders these components. If this page gets a lot of traffic, regenerating the product details, fetching all reviews, and querying for related products on every request can become a bottleneck.
Fragment caching lets us cache parts of this view. We can wrap specific sections with cache and a unique key.
<%# app/views/products/show.html.erb %>
<h1><%= @product.name %></h1>
<div id="product-details">
<%= cache ["v1", @product] do %>
<%= render 'details', product: @product %>
<% end %>
</div>
<div id="reviews">
<h2>Customer Reviews</h2>
<%= cache ["v2", @product, "all_reviews"] do %>
<%= render @reviews %>
<% end %>
</div>
<div id="related-products">
<h2>Related Products</h2>
<%= cache ["v3", @product, "related"] do %>
<%= render @related_products %>
<% end %>
</div>
Here, we’re caching the product-details using the @product object as part of the cache key. If the product itself hasn’t changed (its id and updated_at timestamp are the same), Rails will use the cached version. For reviews, we use a slightly different key. If the product and the string "all_reviews" haven’t changed, the cached content is served. For related products, we use ["v3", @product, "related"]. The "v1", "v2", "v3" are versioning strings. If you change the logic or the view associated with a cached fragment, you increment the version number, forcing a cache invalidation and regeneration.
The cache helper automatically generates a cache key based on the objects you pass. For ActiveRecord objects, it typically uses their id and updated_at timestamp. When the cache block is rendered, Rails checks if a cache entry exists for the generated key. If it does, it returns the cached content. If not, it renders the block, stores the result in the cache, and then returns it.
The core problem fragment caching solves is reducing redundant computation and I/O. Instead of querying the database, serializing data, and rendering templates for parts of a page that rarely change, you serve them directly from a fast in-memory store like Redis or Memcached. This significantly improves response times and reduces server load, especially for high-traffic pages or components that are displayed on many pages.
The mental model is simple: identify expensive-to-render or static parts of your page and wrap them in cache blocks. The key you provide is crucial for cache invalidation. If the data backing the fragment changes, the cache key must change to ensure fresh data is served. This is why including the model object (which has an updated_at timestamp) or other relevant identifiers in the cache key is common practice.
When you use cache @product, Rails generates a key like views/v1/products/123-20231027100000. If @product.updated_at changes, the key changes. If you have a list of items, cache ['items', items] might generate views/v1/items/items-abcdef123456. If any of the items in the items collection change their updated_at timestamp, the generated key might not change unless you explicitly include something that reflects the state of the collection itself, like a digest.
A common pattern for collections is to use cache items do ... end. This generates a cache key that includes a digest of the collection’s contents, automatically invalidating the cache if any item within the collection changes. This is often more robust than manually managing keys for dynamic collections.
The next concept you’ll encounter is Russian Doll Caching, where cached fragments are nested within other cached fragments, and invalidating the outer cache automatically invalidates all inner caches.