Caching is a generic approach to speed up processing time in software. A common pattern used in Rails is to cache already rendered markup to save rendering time.

Basically, there are two concepts for view caching in Rails.

  • Page caching saves the complete page. This is good if your page doesn’t have dynamic parts.
  • Fragment caching which caches only parts of the page – usually, a more complex approach since you have to manage caching for multiple fragments.

Fragment caching

Rails supports fragment caching by letting you define cached parts in the view. You’d use the #cache method for that.

<% cache do %>
  All items in your cart:
  <%= render :partial => "items", 
    :collection => current_user.items %>
<% end %>

This works fine, fragments get rendered only if there’s a cache miss. Three problems with this approach.

  • You’re cluttering your views with caching declarations. It is handy to quickly define an area as cached, however, what if you suddenly change your cache key generation? You have to work through your views for a low-level task.
  • You shift responsibilities – the wrapping view defines the cached fragment. Instead of placing knowledge into the fragment, you aggregate all informations needed for caching on the outside.
  • Since you usually need to adjust the cache key for each fragment, you’re putting expiration logic into your views. This is definitely a no-go in MVC.

Let’s see how caching works in cells view components, which has a much simpler approach.

View caching in Cells

In cells there simply is no fragment caching. As cells are fragments caching happens on the class layer.

class CartCell < Cell::Rails
  cache :show
 
  def show(items)
    @items = items
    render
  end

In order to render the item list “fragment” you’d call #render_cell in your controller view.

  All items in your cart:
  <%= render_cell :cart, :show, current_user.items %>

Zero knowledge about caching in the view at all. Everything from cache key generation to actually marking fragments as cached happens inside the component, which knows best about its caching needs.

No fragment caching?

The first thing worth discussing here is: What if you want to cache a part of a cell, only? A fragment of a fragment, so to say.

You’d model this as another cell state. Period.

Let’s assume the item list in the cart should have a cache entry for each item.

class CartCell < Cell::Rails
  #cache :show
  cache :item do |cell, item|
    item.id
  end
 
  def item(item)
    @item = item
    render
  end
 
  def show(items)

Now, the cell’s show.erb view would call the #item state.

  You have <%= @items.size %> in your cart.
 
  <% @items.each do |item| %>
    <%= render({:state => :item}, item) %>

Again, the wrapping show state doesn’t know anything about the item view’s internal caching. Notice how we use render :state to invoke another cell state.

Using versioners

Ok, now, each item gets its very own cache entry. How does that work?

class CartCell < Cell::Rails
  cache :item do |cell, item|
    item.id
  end

Step-wise.

  • Every time the #item state is invoked the versioner block is called.
  • It receives the item instance (passed from render :state).
  • Now the versioner computes a cache key – the return value from the block is appended to the state’s generic cache key. In our example, we’d generate keys like cells/cart/item/1, and so on.

Next time the #item state is invoked with item no. “1” it won’t get rendered again since its cached view is returned instead.

Expiring caches

Expiring caches is one of the two real problems in computer science. The simplest approach is timed expiries.

class CartCell < Cell::Rails
  cache :item, :expires_in => 10.minutes do |cell, item|
    item.id
  end

Each item view gets flushed after 10 minutes. Things can be so simple.

Some people unlike me like sweepers. Cells ships with a sweeper method.

class ItemSweeper < ActionController::Caching::Sweeper
  observe Item
 
  def after_update(record)
    expire_cell_state CartCell, :item, record.id
  end

Just use expire_cell_state to swipe views item-wise from the cache. Thanks, Joe, for this example.

Even regular expression expiry works with expire_fragment! I didn’t even know this exists.

  expire_fragment %r(cells/cart/item/999)

This will remove items from item group 999, only – presuming these item ids start with 999.

Test your caching!

There are people telling you not to test your caching. These people either have bullet-proof integration tests, or are just lame. There is no excuse for missing caching tests.

I’d refactor the versioner to an instance method first, in order to unit-test.

class CartCell < Cell::Rails
  cache :item, :item_versioner
 
  def item_versioner(item)
    item.id
  end

Yeah, both blocks and methods work with cache. Let’s test that.

class CartCellCachingTest < Cell::TestCase
  test "item versioner appends id" do
    assert_equal "1", cell(:cart).item_versioner(@one)
  end

Testing if caching actually works is another good idea.

class CartCellCachingFunctionalTest < Cell::TestCase
  setup do
    ActionController::Base.perform_caching = true
    ActionController::Base.cache_store.clear
    @cell = cell(:cart)
  end
 
  teardown do
    ActionController::Base.perform_caching = false
  end

Enable caching and always clear the cache store – it’s good practice.

Testing cache writes

We should also check whether rendering a certain state alters the cache.

class CartCellCachingFunctionalTest < Cell::TestCase
  test "rendering item should write to cache" do
    expected_key = @cell.class.state_cache_key(:item, 1)
    assert_equal "cells/cart/item/1", expected_key

First, I assert we got correct keys.

    assert_not Cell::Base.cache_store.read(expected_key)
    render_cell(:cart, :item, @one)
    assert Cell::Base.cache_store.read(expected_key)
  end

Then I actually test if we got a cache write.

I agree that these tests are pretty much bound to the caching implementation in Cells and Rails. Also, I miss the ability to test cache reads and I’m waiting for inspiration from the community.

What could help writing real caching tests could be an #assert_caches or so. Let’s wait for reactions to this post.

Get OOP back to your views!

Ok, now we learned how simple Cells’ state caching is.

To sum things up: The previous examples have – at least – two cool advantages in comparison to fragment caching.

  • Although we cache fragments, we still have any caching knowledge in the class and not cluttered over the views itself.
  • Our “fragment” is an object-oriented component – you could inherit from the CartCell and override the view while keeping the caching mechanics. This is definitly something the Rails community – used to monolithic controllers – still has to discover and I absolutely agree that this is an evolving process.

However, I hope this post helps you when it comes to caching views. Don’t forget to check out the docs as well.

Tags: ,

15 Responses to “Rails Misapprehensions: Caching Views is Not the View’s Job!”


  1. Anony

    A little much with the bold no?

    If anything this “cell” based approach seems 10x more complex and not Rails like than just using the cache helper. It’s the views job to cache content, I don’t see what is so hard to understand about that.


  2. nick

    @Anony: The problems with Rails fragment caching are described pretty detailed in this post. Please, be so kind yourself, and be more precise when saying this “approach seems 10x more complex”, and explain what scares you.

    I think the biggest problem is that Cells forces you to rethink your view architecture. While you could simply use a quick cache helper in the view Cells urges you to model that fragment into an object-oriented method. And this might look strange to some people, but it is the logical conclusion for a real MVC framework with components. Oi!


  3. nick

    Cool! Ruby5 features this article in the show #150. Thanks, Gregg and Nathan!

  4. This has not generally been a problem for me, actually:

    > You shift responsibilities – the wrapping view defines the cached fragment. Instead of placing knowledge into the fragment, you aggregate all informations needed for caching on the outside.

    My experience is generally that the including page knows better what kind of staleness it can tolerate when using time-based expiry. For example, my home page can tolerate a few minutes of staleness, while the admin UI can tolerate none.

    Besides, can’t you just push the cache block down a level if you want the sub-view to have responsibility?


  5. nick

    @Marcel: I see your points. In a typical Rails setup with a monolithic page, you wouldn’t need the fine-grained mechanics I describe in the post.

    Nevertheless, keep in mind that a Cell shouldn’t be mapped to an entire page, but to single “blocks” on that page. And imo, a “Recent Comments” block knows best about its staleness, and not the surrounding page.

    I like that you bring the Admin UI as an example. You have multiple “blocks” there with different caching needs for each of those – how do you handle that? A #cache block per “block”?

    You’re free to push things down to the partial – but do you really want that kind of “stacked” caching declarations?

    BTW: The point you’re refering to is definitely the weakest in my argument list :-)

  6. @nick in light of Marcel’s use case, is there a way to render a cell and explicitly bypass the cache? I.e. Could the cell have a 10 minute cache but the admin interface request a fresh copy anyway (and re-cache that)?

    It’s an interesting point, but personally I find it hard to believe that you’d have the same forward facing (read: public) components as you would on your admin interface, anyway…

  7. Nick,

    This is great! Rails caching is something that is easy to get started with, but also easy to get tangled up in very quickly.

    For people not familiar with Cells, and those the came over from the ruby5 podcast, you might have come to this post expecting to see some code snippet that is going to make your vanilla Rails site cache everything better.
    But this is more of an example of how caching can be simpler when pulled out of the view, and the view is composed of small reusable components. More of a ‘food for thought’ post, than a how-to, but if you are about to go down the path of adding more complex caching in your app, you would do well to make it as simple as you can.

    The more I see about Cells, the more I want to try it out.


  8. nick

    @Bodanil: Sorry for the late answer. Cells – being good Rails citizens – comply with config.perform_caching so you could use that flag to disable caching in backends.

    I’m not a fan of this since you’re fiddling with a global configuration variable, but I’m sure it’s kinda easy to extend Cells to make them disable caching under a certain route or so. Interesting point, definitely!

    @John: Thanks! You’re right, this ain’t no introducing post but a discussion focused on a different approach, so I understand people may be disappointed at first sight (“WTF- I don’t wanna use Cells for caching my Rails views?!”). I’m happy you like it!

    This is all I remember:


  9. Cyrille

    Nice post ! Everything works as intended even after a “conversion” to rspec :)

    Just wondering how you will test the expire_cell_state in a sweeper such as:

    class SpotSweeper “homepage”)
    Cell::Base.cache_store.read(“cells/spot/edito/homepage”).should_not be_nil
    # so far working…
    # Call the sweeper and clean the cache
    SpotSweeper.instance.after_save(@spot)
    # test the cache has been wipe out
    Cell::Base.cache_store.read(“cells/spot/edito/homepage”).should be_nil
    end

    However I’d no success so far. I can’t find a way to call a force the sweeper callback in my specs


  10. Cyrille

    Formatting mess sorry

    The sweeper

    class SpotSweeper &lt; ActionController::Caching::Sweeper
      observe Spot
     
      def after_save(record)
        [:edito, :header, :menu].each do |state|
          expire_cell_state SpotCell, state, record.spot_area.identifier
        end
      end
    end

  11. Cyrille

    and the test

    it "should clear the cache" do
      render_cell(:spot, :edito, :area =&gt; "homepage")
      Cell::Base.cache_store.read("cells/spot/edito/homepage").should_not be_nil
      # so far working...
      # Call the sweeper and clean the cache
      SpotSweeper.instance.after_save(@spot)
      # test the cache has been wipe out
      Cell::Base.cache_store.read("cells/spot/edito/homepage").should be_nil
    end

  12. nick

    Cyrille: Maybe try to let Cells compute the cache key?

    it "should clear the cache" do
      key = cell(:spot).
        state_cache_key(:edito, :homepage)

    Does that help? However, we should add a test method to make this easier!


  13. DHH

    This is not prudent.


  14. Steve

    This is like the “Russian Doll Caching” in Rails 4? Why didn’t Rails just use Cells??

Leave a Reply