page caching in Rails

27 Mar 2014 - New York

Page caching

Page caching is a coarse approach to caching whereby an entire page is rendered once and stored on the web server so that subsequent requests don’t even reach the application server.

While this approach offers a significant performance boost, it’s only appropriate for stateless pages where all visitors are treated the same.

Examples: Weblogs, wikis, static pages. Anything that doesn’t change based on, say, whether or not the user is logged in.

Nonexamples: Pages requiring authentication for any other before_action (since requests never reaches a before_action / before_filter). Pages that are rendered differently for different users.

In Rails 4, page caching must be enabled via the actionpack-page_caching gem.

# config/environments/development.rb
config.action_controller.perform_caching = false  # enables caching locally
# Gemfile
gem 'actionpack-page_caching'

Set the page_cache_directory:

# config/application.rb
config.action_controller.page_cache_directory = Rails.root.join('public', 'cache')

And declare which actions render templates that can be cached in their entirety:

# app/controllers/posts_controller.rb
class PostsController < ActionController::Base
  caches_page :index, :show
  # . . .
end

Cache expiration

To expire a cached page, call expire_page whenever any of its data becomes outdated. For example,

class PostsController < ActionController::Base
  caches_page :index, :show
  after_action :clear_posts_cache, only: %i(index show)
  # . . .
  private

  def clear_posts_cache
    expire_page action: :index
    expire_page action: :show, id: @post
  end
end

Sweepers

But what happens when you add a comments controller and need to expire a post’s page cache when a new comment on it is created? You could move the defintion of clear_posts_cache one level up to ApplicationController or to a namespace both have access to, such as a concern.

A cleaner, more object-oriented way is to use a Sweeper. These too have been removed from the Rails core with Rails 4.0, so a bundle install is in order:

# Gemfile
gem 'rails-observers'

Sweepers are responsible for observing models and expiring caches when an assigned model’s attributes change value.

# app/sweepers/post_sweeper.rb

class PostSweeper < ActionController::Caching::Sweeper
  observe Post, Comment

  def after_save(record)
    post = post_from(record)
    clear_posts_cache_for(post)
  end

  def after_destroy(record)
    post = post_from(record)
    clear_posts_cache_for(post)
  end

  private

  def clear_posts_cache_for(post)
    expire_page(controller: :posts, action: %i(index show), id: post
  end

  def post_from(record)
    record.is_a?(Post) ? record : record.post
  end
end

You can then call this Sweeper via a callback in any controllers that might change the pages being cached:

class PostsController < ApplicationController
  cache_sweeper :post_sweeper, only: %i(edit destroy)
end
class CommentsController < ApplicationController
  cache_sweeper :post_sweeper, only: %i(create edit destroy)
end

Stateful Pages

AJAX as a workaround

One obvious, if slightly iffy, way to make page caching work with pages that have some kind of state is to use AJAX calls to dynamically update only the required data on the page. This might be fine if it’s a small amount of data (say, a login link), but in most cases there are better approaches to take.

Action Caching

So what happens when you are working with pages that need to hit controller filters? That’s where our next strategy comes in: action caching. More on that next time.

These notes were compiled from the Rails guides.