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.