Skip to content

Instantly share code, notes, and snippets.

@jonwaghorn
Last active August 29, 2015 14:26
Show Gist options
  • Save jonwaghorn/0a6118f6f44cf8079eea to your computer and use it in GitHub Desktop.
Save jonwaghorn/0a6118f6f44cf8079eea to your computer and use it in GitHub Desktop.
Overview of caching with Rails 4, with links to more

Caching and Rails

In the big world of webpage serving, Rails and Ruby is a bit slow, but it's good, wholesome, and comfortable! We like Ruby on Rails at 3months.

Caching is a technique for doing work once, and reusing that work later instead of doing it again.

The trickiest part of caching is knowing when the cached thing has changed, i.e when to expire the cache.

You can cache at many levels, client...server...web-server...database. This is mostly about the web-server/Rails level.

Nate Berkopec post

Excellent post by Nate Berkopec. Recently linked to by @OliBridgman, and very timely!

Useful shit: http://www.nateberkopec.com/2015/07/15/the-complete-guide-to-rails-caching.html

TL;DR

  • Caching is good
  • Read your logs
  • Set performance goals
  • Only target the poorly performing parts of your website
  • Use fragment caching
  • Choose a cache storage strategy based on speed to access vs distributed share-ability

Caching with Rails

Definitive shit: http://guides.rubyonrails.org/caching_with_rails.html

  • Page caching (moved out since Rails 4)
  • Action caching (moved out since Rails 4)
  • Fragment caching, key-based, the new preferred way

Usually used only in Production. But you should test it sometimes, like in development or whatever...

To enable caching, set environment file:

config.action_controller.perform_caching = true

Key-based cache expiration

<% todo = Todo.first %>
<% cache(todo) %>
  ... a whole lot of work here ...
<% end %>

views/todos/123-20120806214154/7a1156131a6928cb0026877f8b749ac9

Russian Doll caching

Russian Doll caching is just a slightly fancy use of key-based caching

<% cache(["todo_list", @todos.map(&:id), @todos.maximum(:updated_at)]) %>
  <ul>
    <% @todos.each do |todo| %>
      <% cache(todo) do %>
        <li class="todo"><%= todo.description %></li>
      <% end %>
    <% end %>
  </ul>
<% end %>

It's important that the outer cache key includes a reference to all the inner cached items. If an inner cached item changes then that inner item will get rebuilt next time, and the outer cache will also get rebuilt, but significantly it gets rebuilt from already cached things plus the one newly rebuilt item.

Creative NZ

2 Real world examples I used recently:

The home page

http://www.creativenz.govt.nz

<div class="m-grid_page--content-block home">
  <%= render :partial => 'carousel' %>
  <%= render :partial => 'posts' %>
</div>
<% cache(['homepage', @recent_posts.map(&:id), 
                      @recent_posts.maximum(:updated_at), 
                      @carousel_items.map(&:id), 
                      @carousel_items.maximum(:updated_at)]) do %>
  ...the thing above...
<% end %>

Included in the thing above are:

Carousel

<div class="m-carousel">
  <% @carousel_items.each do |carousel_item| %>
    <% cache(carousel_item) %>
      ...stuff...
    <% end %>
  <% end %>
</div>

Posts

<% @recent_posts.each |post| %>
  <% cache(post) %>
    ...stuff...
  <% end %>
<% end %>

The navigation menu

aside: study your log files for time used in view/db, introduced nicely in the Nate Berkopec post.

The menu was pretty slow to render, approx 1 second. And. It's. On. Every. Single. Damn. Page.

E.g.

http://www.creativenz.govt.nz
http://www.creativenz.govt.nz/about-creative-new-zealand
http://www.creativenz.govt.nz/find-funding/other-sources-of-funding

It's similar, but different. So we can't cache just 1 menu, each page (approx 80) has it's own unique navigation menu. It was still worthwhile to cache these as the content changes only slowly.

<div role="tree" id="navigation_menu">
  <nav class="m-navigation--nav">
    <ul class="m-navigation--items_level1">
      <% Page.roots.visible.positioned.each do |page| %>
       <%= main_nav_link(page, @page) %>
      <% end %>
    </ul>
  </nav>
</div>

With caching

<% cache(['menu', @page.title_en, @page.path_ids, Page.last_changed.updated_at]) do %>
  ...the thing above unchanged...
<% end %>

Most pages are db content with title_en and id in a tree structure (turtles all the way down through main_link_nav()), but there are some special pages that get created temporarily and they always have @page.title_en which is why this is in the cache key. @page.path_ids is helper to get an array of ids for the tree branch from root to @page inclusive.

Probably could have done some Russian Doll caching but testing this gave page load times of around 25ms, win! I think some good advice is to only cache the minimum to meet your performance goals, read Nate's post above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment