Rails Caching

18 Apr 2014 By: Greg Molnar

I've built a Kanban board as a side project and I want to share how much performance boost I was able to give it by implementing a Russian Doll Caching.
The app itself is a typical kanban board where you can setup different stages for the tasks and they are in the column of the appropriate stage. I have a project where I have 4 stages: To-do, In progress, Waiting for review, Done. On this board I have 138 tasks in the moment. According to Rack Metrics this page takes between 1485 and 1678 ms to get ready on the server side. This is way too slow in my opinion so I will dig in a bit more. I randomly picked one of the request to this route and I saw there the following:

Duration:  1598.14ms
View runtime:   1485.152174ms
DB runtime:   78.776503ms
Render calls:   140

There are 140 render calls because I use partials and every task on the board triggers a render call. This is a great place to utilise some caching.

First, I need to make sure caching is enabled and set to mem_cache_store by putting this line into config/production.rb:

config.action_controller.perform_caching = true
config.cache_store = :mem_cache_store

I use memcached for storage because I will have a lot of cache entries, and storing them on the filesystem would cause a lot of IO and it would not give me too much performance win. Another reason to use memcached is it's ability to invalidate the oldest keys first when it runs out of space.

Next I need to go to my view and wrap the render calls into a caching block: This:

<% status.tasks.each do |task| %>
  <%= render task %>
<% end %>

Will become this:

<% status.tasks.each do |task| %>
  <% cache(task) do %>
    <%= render task %>
  <% end %>
<% end %>

As you see I pass an object to the cache call, which calls the cache_key method and that will generate a key based on the updated_at field of the object, so our cache will be invalidated automatically when it needs to. Let's see how much improvement this caused. After hitting refresh a few times my response time is between 129 and 159ms. It is around 10% of what it was before which is a huge improvement. If a task is updated only that one will be rendered again so the overall speed of this section is dramatically got better. One important thing to do is to change my relation so when I subtask or comment is added to the task it's cache will be invalidated:

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true, touch: true
end

With touch true every time a comment is created or updated it will update the task's updated_at field so the cache will be invalidated.
I already made my app perform better but there is one more thing I can do to make it even faster. I could cache the whole view in a fragment and bust the cache when any of the tasks has changed. To make this work I created a helper method to generate a cache_key based on the number of tasks and the updated task:

module ProjectsHelper
  def cache_key_for_tasks
    count          = Task.count
    max_updated_at = Task.maximum(:updated_at).try(:utc).try(:to_s, :number)
    "tasks/all-#{count}-#{max_updated_at}"
  end
end

In the view:

<% cache(cache_key_for_tasks) do %>
  ...
  # rest of the file
<% end %>

With this in place I made around 30ms improvement.
As you see with clever caching your Rails app can be a lot faster and use less resources.
I hope you enjoyed the article. UPDATE: A second part of this acrticle available at: http://www.rubytutorial.io/rails-caching-again/

PS: If you want to get updates from me please subscribe to my email list.
I hate spam as much as you do, so I won't send you anything else than Ruby/Rails related updates occasionally, and of course you can unsubcribe anytime.