Caching and Performance Optimization

Ruby on Rails Caching and Performance Optimization

Featured Snippet: Ruby on Rails caching significantly improves application performance by storing page content or fragments. Action caching intercepts requests at the controller level to allow authentication filtering before serving cached content. Fragment caching stores specific granular UI components like shopping carts. Finally, cache sweepers act as observers to automatically expire stale cache fragments when underlying database records are updated.

In this fourth part of a six-part series on optimizing the performance of a Ruby on Rails application, you'll learn about action caching, fragment stores, and more. This article is excerpted from chapter 13 of the book Practical Rails Projects, written by Eldon Alameda (Apress; ISBN: 1590597818).

Understanding Action Caching

Action caching is the second level of caching in Rails. With action caching, the whole page output is still cached, but this time, the request goes through ActionController, and thus the filters are run before the rendering. The most important consequence of this is that you can use action caching even for pages that need authentication.

Action caching is turned on in the same way as page caching. Add caches_action :index to the controller, and call expire_action :index to expire the action. You won’t get the same raw speed as with page caching, but you will get more flexibility with filtering the requests. The speed would still be plenty fast.

Action caching shares many, but not all, of the problems with page caching. There is still no way to make the page contents dynamic, and complete personalization isn’t possible (although action caching can use the user ID as a key in the cached page).

If the results of page caching are stored in the file system, where do the action caches go? The answer is that it depends. Action caching uses internally the third built-in caching scheme in Rails, fragment caching.

Implementing Fragment Caching

Fragment caching is the most granular of the standard caching mechanisms in Rails. With it, you can cache parts of a page. For example, you could cache the contents of a shopping cart like this (in app/views/layouts/application.rhtml):


<% if @cart %>
  <% cache(:controller => "cart", :action => "show", :id => @cart) do %>
    <div id="shopping_cart">
      <%= render :partial => "cart/cart" %>
    </div>
  <% end %>
<% end %>
    

This would cause the contents of the cache block to be cached, and we could avoid a perhaps expensive database trip on every request a particular user makes.

The cache method takes a hash as its parameter and uses url_for to build a URL to be used as a key to the cached item. Needless to say, this should be unique. Note that it doesn’t have to be a real, existing URL. In our case, for example, there is no action called show in CartController. However, the cache key of a stored cart would be something like emporium.com/cart/show/179.

Cached fragments are expired with the expire_fragment method, which takes a hash as its argument, similar to the argument for the cache method. In our case, we need to expire the fragment whenever the shopping cart is changed—when we add or remove books to the cart, clear the cart, or check out.

Let’s start with CheckoutController (in app/controllers/checkout_controller.rb):


def place_order
    @page_title = "Checkout"
    @order = Order.new(params[:order]) 
    @order.customer_ip = request.remote_ip 
    populate_order

    if @order.save
      if @order.process
        flash[:notice] = 'Your order has been submitted and will be processed immediately.'
        session[:order_id] = @order.id
        # Empty the cart 
        @cart.cart_items.destroy_all 
        expire_fragment(:controller => "cart",
                        :action => "show",
                        :id => cart)
        redirect_to :action => 'thank_you' 
      else
        flash[:notice] = "Error while placing order '#{@order.error_message}'"
        render :action => 'index'
      end
    else
      render :action => 'index'
    end
  end
    

Now whenever an order is processed and the shopping cart is cleared, the cached cart fragment is expired.

Utilizing Cache Sweepers

For CartController, we use a different approach—a cache sweeper. A cache sweeper is a special kind of an observer. It observes the lifeline of an object and can sweep cached stuff when specific changes (such as create, update, or destroy) are made to the object in question. Create a file called cart_sweeper.rb in app/models and add the following code to it:


class CartSweeper < ActionController::Caching::Sweeper
  observe Cart, CartItem

  def after_save(record)
    cart = record.is_a?(Cart) ? record : record.cart
    expire_fragment(:controller => "cart",
                    :action => "show",
                    :id => @cart)
  end
end
    

You can see that the sweeper looks just about the same as a normal observer. In this case, we observe both the Cart object and the CartItem objects that belong to it. When either kind of object is saved, we find the relevant Cart object and expire the fragment that belongs to that cart. To make the sweeper work, we need to call it in CartController (app/controllers/cart_controller.rb):


class CartController < ApplicationController
  cache_sweeper :cart_sweeper
  before_filter   :initialize_cart
...
    

That’s all. Since we want the sweeper to work on all the actions in CartController, we don’t have to specify anything else. If we wanted to restrict the sweeper to only certain actions, we could use the :only parameter for that.

Frequently Asked Questions (FAQ)

What is action caching in Ruby on Rails?

Action caching is the second level of caching in Rails. The whole page output is cached, but the request goes through ActionController, so filters are run before rendering, allowing its use for pages that require authentication.

What is fragment caching?

Fragment caching is the most granular of standard caching mechanisms in Rails. It allows caching specific parts of a page, such as a shopping cart, to avoid expensive database queries on every request.

What is a cache sweeper in Rails?

A cache sweeper is a special observer that monitors the lifecycle of an object. It can clear or 'sweep' cached content when specific changes like create, update, or destroy occur.