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