Rails 3 full page caching on Heroku

Recently I implemented full page caching on this blog. Not because of the insane amount of traffic I've been receiving (10 hits a day) but because it was something I wanted to experiment with and I also wanted to squeeze some more speed out of this CMS.

Using Turbolinks the pages were loading fast but now with the full page caching clicking on a link instantly shows the new page. You don't even have time to blink!

Page Caching

So I went about my way implementing Rails page caching which basically caches the entire page including the layout to a static html file in your public directory. This has the advantage of not hitting the rails stack and it's called directly from the web server (nginx/apache). There are a few gotchas when implementing page caching in rails. First you can't really have anything dynamic which for this site doesn't seem like to much of a problem at first, although when you update a post or someone leaves a comment the web server continues to serve up the static html file leaving you scratching your head.

Rails has some awesome utilities to get around this. There is the expire_page method which allows you to pass in a route to remove the static file. This is useful after updating a post or when someone leaves a comment. At first I created a helper method to clear the cache until I came across Rail's cache sweepers. These allow you to observe a model and run methods after save, update and destroy. So I moved all of my caching logic into the sweeper so that whenever a post is updated the cache is cleared for the post and any other page that contains post data (home page, archives, etc.).

Here is the sweeper I originally used:

class PageSweeper < ActionController :: Caching :: Sweeper observe Page def sweep ( page ) # Page Caching expire_page pages_path expire_page page_path ( page . slug ) expire_page '/' expire_page archives_path FileUtils . rm_rf " #{ page_cache_directory } /pages" end alias_method :after_create , :sweep alias_method :after_update , :sweep alias_method :after_destroy , :sweep end

So everything is running quick, Rails doesn't have to serve up pages anymore but wait... unfortunately Heroku doesn't support page caching.

Heroku has an ephemeral file store, so while page caching may appear to work, it won’t work as intended.

https://devcenter.heroku.com/articles/caching-strategies#page-caching

I didn't realize this until after checking out the logs and doing a bit of research. After reading their caching strategies page I decided to use action caching.

Action Caching

Action caching works similarly to page caching although it still hits the Rails stack. There are some advantages to using the action cache over the page cache such as being able to access your before filters to determine if a user's logged in or whatever you usually do in the before filter.

Action cache uses in memory stores (memcache usually) to cache the html of the page. You can opt-out of caching the layout if you want to keep certain things dynamic such as if a user is logged in and just cache the html of the action itself.

After implementing the action cache I really couldn't tell the difference between it and page caching. It was still super fast and made clicking on links seem like they were static html files. Action caching uses an almost identical API to page caching with a few small differences.

I decided to keep both page and action caching in case one day I move away from Heroku to a host with a standard file system. I ended up creating some configuration to determine whether to use page or action caching.

In the pages controller:

caches_page :index , :show , :archives if Blog . cache == :page caches_action :index , :show , :archives if Blog . cache == :action

And I updated my sweeper method to clear the cache based on the configuration:

class PageSweeper < ActionController :: Caching :: Sweeper observe Page def sweep ( page ) if Blog . cache == :page # Page Caching expire_page pages_path expire_page page_path ( page . slug ) expire_page '/' expire_page archives_path FileUtils . rm_rf " #{ page_cache_directory } /pages" elsif Blog . cache == :action # Action Caching expire_action controller : '/pages' , action : :index expire_action " #{ request . host } / #{ page . slug } " expire_action controller : '/pages' , action : :archives # Remove pages cache pages = ( Page . published_posts . count . to_f / Blog :: POSTS_PER_PAGE . to_f ) . ceil 1 . upto ( pages ) { | p | expire_action " #{ request . host } /pages/ #{ p } " } end end alias_method :after_create , :sweep alias_method :after_update , :sweep alias_method :after_destroy , :sweep end

Loading dynamic partials

Since originally I was using full page caching I decided to load certain parts of the page via ajax and partials. For the most part this just checks if the admin is logged in and will show extra links in the navigation and options to moderate comments. I am also loading in the recent posts in the side bar via ajax so that I don't have to clear every page from the cache when adding a new post.

I will write another post on some of the cool techniques I used to only load the partials once via ajax per viewing session, since Turbolinks doesn't do full page refreshes.

More reading