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.
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
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
<% todo = Todo.first %>
<% cache(todo) %>
... a whole lot of work here ...
<% end %>
views/todos/123-20120806214154/7a1156131a6928cb0026877f8b749ac9
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.
2 Real world examples I used recently:
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:
<div class="m-carousel">
<% @carousel_items.each do |carousel_item| %>
<% cache(carousel_item) %>
...stuff...
<% end %>
<% end %>
</div>
<% @recent_posts.each |post| %>
<% cache(post) %>
...stuff...
<% end %>
<% end %>
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.