Fragment caching with lambda's
Here’s some commonly seen Rails controller code:
Your controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class PostsController < ApplicationController def index @posts = get_posts respond_to do |format| format.html format.js { render :json => @posts} format.xml { render :xml => @posts} end end private def get_posts # some complex query that # returns some posts, say... end end |
Technically the #get_posts method should be in the Post model,
but bear with me, I’m trying to prove a point.
index.html.erb
1 2 3 4 5 |
<h2>Posts!</h2> <% @posts.each do |post| -%> <h3><%=link_to(post) %></h3> <% end -%> |
So that’s pretty standard. The site’s been doing great and everyone who is
anyone is checking out the index page!
Unfortunately your server is getting steadily dragged down and after
some profiling you realize that the main bottleneck is the #get_posts
method.
Not all is lost, you’ve heard that caching can help! Sadly the rest of your
page (that is, various bits of your layout) is quite dynamic and cannot be
cached globally so page and action caching isn’t going to help.
Luckily Rails supports fragment caching which allows you to cache a specific
bit of ERB. So your view becomes:
index.html.erb
1 2 3 4 5 6 7 |
<h2>Posts!</h2>
<% cache do -%>
<% @posts.each do |post| -%>
<h3><%=link_to(post) %></h3>
<% end -%>
<% end -%>
|
So that looks great, you profile again and notice that from the second
request onwards, the server is indeed breathing a little easier…but
not nearly as much as you expected.
The reason is that the database queries are still being run in ‘#getposts’,
and the posts assigned to @posts. The fragment caching just skips looping
through the @posts array, replacing the entire ‘cache do … end’ block
with the HTML that got generated during the previous request.
This leads people to unhappy action like this:
The associated helper
1 2 3 4 5 6 7 8 |
module PostsHelper def get_posts # some complex query that # returns some posts, say... end end |
Your controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class PostsController < ApplicationController def index respond_to do |format| format.html format.js { render :json => get_posts} format.xml { render :xml => get_posts} end end private def get_posts # some complex query that # returns some posts, say... end end |
index.html.erb
1 2 3 4 5 6 7 |
<h2>Posts!</h2>
<% cache do -%>
<% get_posts.each do |post| -%>
<h3><%=link_to(post) %></h3>
<% end -%>
<% end -%>
|
Don’t get me wrong, this works. Also, in this rather simple case you can
use Rails’ #helper_method function to remove the duplication.
However, something worth looking at when you run up against more involved
problems of this kind is to use lambda’s for lazy evaluation in the view:
Your controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class PostsController < ApplicationController def index @posts_finder = find_posts respond_to do |format| format.html format.js { render :json => @posts_finder.call} format.xml { render :xml => @posts_finder.call} end end private def find_posts Proc.new do # some complex query that # returns some posts, say... end end end |
index.html.erb
1 2 3 4 5 6 7 |
<h2>Posts!</h2>
<% cache do -%>
<% @posts.call.each do |post| -%>
<h3><%=link_to(post) %></h3>
<% end -%>
<% end -%>
|
That way @posts is lazily evaluated and you’ve cleanly removed your
bottleneck.
Let me know what you think!
Gustav