loading...

Rails Caching, Pt. 1

katkelly profile image Katherine Kelly Updated on ・4 min read

I've been exploring caching recently, mostly at a high level to better understand the importance of a cache and what goes on behind the scenes (check out Cache Me If You Can and Implementing an LRU Cache for those posts!).

It's widely understood that a cache is often the most effective way to boost an application's performance. Web sites that feature a single server and a single database can sustain a load of thousands of concurrent users with caching.

If you have a Ruby on Rails application, Rails offers a set of caching features out of the box. This post is going to be part one of a dive into the most common cache techniques and how you can implement caching in development to check it out!

Caching in Development

Since caching is only enabled by default in the production environment, we'll need to enable caching directly in the local development environment.

But, Rails makes it super easy now (on Rails 5 and above) to set up caching in development. You just need to run rails dev:cache to toggle caching on and off and if it's turned on, Rails will create a tmp/caching-dev.txt file. You also won't need to restart the server, as one would have had to do with older versions of Rails. Here is what it looks like in config/environments/development.rb:

# Enable/disable caching. By default caching is disabled.
  # Run rails dev:cache to toggle caching.
  if Rails.root.join('tmp', 'caching-dev.txt').exist?
    config.action_controller.perform_caching = true
    config.action_controller.enable_fragment_cache_logging = true

    config.cache_store = :memory_store
    config.public_file_server.headers = {
      'Cache-Control' => "public, max-age=#{2.days.to_i}"
    }
  else
    config.action_controller.perform_caching = false

    config.cache_store = :null_store
  end

Rails Caching Techniques

In Rails, there are three different types of caching techniques: page, action, and fragment caching.

Rails provides fragment caching by default. To use page and action caching, you'll have to add these specific actionpack gems to our Gemfile.

Page Caching

Page caching is a cache approach that stores response bodies in files that the web server can access.

  1. A request to endpoint A arrives
  2. Its response is determined and gets stored in a file B
  3. Next time A is requested, the web server sends B directly

It only applies to GET or HEAD requests with response code 200. This can result in a quick response because the cached endpoints are short-circuited by the web server and don't even reach your Rails application. However, this approach only works for pages that don't need to go through the entire Rails stack. Anything that involves authentication and account-based systems are not good contenders.

As of Rails 4, page caching has been removed, but you can access and use it by adding actionpack-page_caching to your Gemfile.

Action Caching

Action caching is similar to page caching except the incoming web request hits the Rails stack through Action Pack so that any authentication and before filters can be run before the data in the cache is served.

As of Rails 4, action caching has been removed, but you can access and use it by adding actionpack-action_caching to your Gemfile.

Fragment Caching

By default, Rails offers fragment caching. Depending on the needs of your web application, you can utilize fragment caching to cache different parts of the page. This allows flexibility in what parts you want cached and when it should expire. Fragment caching lets a fragment of view logic (partials, widgets, etc.) to be wrapped in a cache block and served out of the cache store when the next request is made.

As an example, if you have a travel app and wanted to cache each destination on a page, the code could like something like this:

<% @destinations.each do |destination| %>
  <% cache destination do %>
    <% render destination %>
  <% end %>
<% end %>

Rails will write a new cache entry with a unique key when your app gets its first request to this page. The key will resemble the following:

app/views/destinations/
1-202006026193031061005000/bea67108094918eeba42cd4a6e786901

The number after destinations is the product_id followed by a timestamp value of the updated_at attribute of the destination record. This timestamp ensures that Rails is not serving stale data. If updated_at has changed, a new key will be generated and Rails will write a new cache to that key. The old key won't be used again. This is key-based expiration in a nutshell.

If the view fragment itself changes (if the HTML changes) the cache fragment will also be expired.

You can also conditionally cache a fragment using cache_if or cache_unless:

<% cache_if beachside?, destination do %>
  <%= render destination %>
<% end %>

Collection Caching

The render method also allows you to cache individual templates rendered for a collection. The below example can read all cache templates at once instead of one by one like above using each.

<% render partial: 'destinations/destination', 
collection: @destinations, cached: true %>

Tested

When I played around with implementing the cache, my initial page load after all of the database queries was 316ms:

Write fragment views/destinations/show:
ea9a60feb104fccbb55d6feb00a7dcb8/hacks/265-20200528222447858679 (0.1ms)

Rendered destinations/show.html.erb within layouts/
application (Duration: 187.9ms | Allocations: 26579)

Completed 200 OK in 316ms (Views: 141.6ms | ActiveRecord:
120.6ms | Allocations: 65663)

When I reloaded the page, the time was cut down to 62ms and the fragments were taken from the cache!

...
Read fragment views/destinations/show:ea9a60feb104fccbb55d6feb00a7dcb8
/hacks/265-20200528222447858679 (0.2ms)

Rendered destinations/show.html.erb within layouts/
application (Duration: 8.2ms | Allocations: 1860)

Completed 200 OK in 62ms (Views: 56.9ms | 
ActiveRecord: 2.0ms | Allocations: 29929)

Next week, I'm going to look into Rails Cache Stores as well as other caching techniques I didn't cover here. (Edit: Rails Caching, Pt. 2 is up!)

Happy caching!

Resources
Caching With Rails: An Overview - Rails Guides
Caching Strategies for Rails - Heroku

Posted on by:

katkelly profile

Katherine Kelly

@katkelly

Coding, eating all the noodles, cheering on the SF Giants

Discussion

markdown guide
 

I just read a recent, and very interesting, post about the cache: value for Collection caching - dev.to/briankephart/collection-cac... This allows for more complex cache keys than just the objects being rendered.

 

That's a really interesting way to get more complex cache keys. Thanks for sharing the post!

 

It’s worth pointing out that collection caches require you to specify the partial name (as you do in the article) even if normally it could be inferred from the collection name.

This isn’t well documented and took me a while to discover.

 

That's a great point. Thanks for sharing!