Speed Things up by Learning about Caching in Rails

Share this article

Speed Things up by Learning about Caching in Rails

As developers, we hear the word “cache” quite often. Actually, it means “to hide” in French (“cache-cache” is a hide-and-seek game). Caching basically means that we store some data so that subsequent requests are being fulfilled faster, without the need to produce that data once again.

Caching can really provide great benefits by speeding up your web application and improving its user experience. Today we are going to discuss various caching techniques available in Rails: page, action, fragment, model and HTTP caching. You are then free to choose one of them or even use multiple techniques in conjunction.

The source code can be found on GitHub.

Turning Caching On

For this demo I’ll be using Rails 5 beta 3, but most of the information is applicable to Rails 4, as well. Go ahead and create a new app:

$ rails new BraveCacher -T

As you probably know, caching is disabled by default in the development environment, so you will need to turn it on. In Rails 5, however, this is done differently. Just run the following command:

$ rake dev:cache

What this command basically does is creates an empty caching-dev.txt file inside the tmp directory. Inside the development.rb there is now the following code:

config/environments/development.rb

if Rails.root.join('tmp/caching-dev.txt').exist?
  config.action_controller.perform_caching = true
  config.action_mailer.perform_caching = false
  config.cache_store = :memory_store
  config.public_file_server.headers = {
    'Cache-Control' => 'public, max-age=172800'
  }
else
  config.action_controller.perform_caching = false
  config.action_mailer.perform_caching = false
  config.cache_store = :null_store
end

This file signals whether to enable or disable caching – you don’t need to tweak the config.action_controller.perform_caching setting anymore. Other differences between Rails 4 and 5 can be found in one of my previous articles.

Please note that if you are using Rails 5 beta, there is a bug when booting server using rails s. To put it simply, the caching-dev.txt file is being removed every time, so for the time being use the following command instead:

$ bin\rails server -C

This bug is already fixed in the master branch.

Okay, now the caching is enabled and we can proceed to the next section. Let’s discuss page caching first.

Page Caching

As someone once said, thank God if you are able to use page caching because it adds a huge boost to your app’s performance. The idea behind this technique is simple: the whole HTML page is saved to a file inside the public directory. On subsequent requests, this file is being sent directly to the user without the need to render the view and layout again.

Unfortunately, such a powerful solution has very limited usage. If you have many pages that look different for different users, page caching is not the best option. Also, it cannot be employed in scenarios where your website may be accessed by authorized users only. However, it shines for semi-static pages.

Starting from Rails 4, page caching has been extracted to a separate gem, so add it now:

Gemfile

[...]
gem 'actionpack-page_caching'
[...]

Run

$ bundle install

Now, tweak your configuration file:

config/application.rb

[...]
config.action_controller.page_cache_directory = "#{Rails.root.to_s}/public/deploy"
[...]

This setting is needed to specify where to store your cached pages.

Let’s introduce a very simple controller and a view to test our new caching technique.

pages_controller.rb

class PagesController < ApplicationController
  def index
  end
end

config/routes.rb

[...]
root 'pages#index'
[...]

views/pages/index.html.erb

<h1>Welcome to my Cached Site!</h1>

Page caching is enabled per-action by using caches_page method. Let’s cache our main page:

pages_controller.rb

class PagesController < ApplicationController
  caches_page :index
end

Boot the server and navigate to the root path. You should see the following output in the console:

Write page f:/rails/my/sitepoint/sitepoint-source/BraveCacher/public/deploy/index.html (1.0ms)

Inside the public/deploy directory, there will be a file called index.html that contains all the markup for the page. On subsequent requests, it will be sent without the need to go through the ActionPack.

Of course, you will also need to employ cache expiration logic. To expire your cache, use the expire_page method in your controller:

expire_page action: 'index'

For example, it your index page lists an array of products, you could add expire_page into the create method. Alternatively, you may use a sweeper as described here.

Action Caching

Action caching works pretty much like page caching, however instead of immediately sending the page stored inside the public directory, it hits Rails stack. By doing this, it runs before actions that can, for example, handle authentication logic.

Action caching was also extracted to a separate gem as well, so add it:

Gemfile

[...]
gem 'actionpack-action_caching'
[...]

Run

$ bundle install

Let’s add a new restricted page with very naive authorization logic. We will use Action Caching to cache the action by calling the caches_action method:

class PagesController < ApplicationController
  before_action :authenticate!, only: [:restricted]

  caches_page :index
  caches_action :restricted

  def index
  end

  def restricted
  end

  private

  def authenticate!
    params[:admin] == 'true'
  end
end

views/pages/restricted.html.erb

<h1>Restricted Page</h1>

config/routes.rb

get '/restricted', to: 'pages#restricted'

Under the hood, this technique uses fragment caching, that’s why you will see the following output when accessing your restricted page:

Write fragment views/localhost:3000/restricted

Expiration is done similarly to page caching – just issue the expire_action method and pass controller and action options to it. caches_action also accepts a variety of options:

  • if or unless to instruct whether to cache the action
  • expires_in – time to expire cache automatically
  • cache_path – path to store the cache. Useful for actions with multiple possible routes that should be cached differently
  • layout – if set to false, will only cache the action’s content
  • format – useful when your action responds with different formats

Fragment Caching

Fragment caching, as the name implies, caches only the part of your page. This functionality exists in Rails’ core, so you don’t have to add it manually.

Let’s introduce a new model called Product, a corresponding controller, view, and route:

$ rails g model Product title:string
$ rake db:migrate

products_controller.rb

class ProductsController < ApplicationController
  def index
    @products = Product.all
  end
end

config/routes.rb

resources :products, only: [:index]

views/products/index.html.erb

<h1>Products</h1>

<% @products.each do |product| %>
  <%= product.title %>
<% end %>

Populate the products table with sample data:

db/seeds.rb

20.times {|i| Product.create!({title: "Product #{i + 1}"})}

And run

$ rake db:seed

Now, suppose we want to cache each product listed on the page. This is done using the cache method that accepts cache storage’s name. It can be a string or an object:

views/products/index.html.erb

<% @products.each do |product| %>
  <% cache product do %>
    <%= product.title %>
  <% end %>
<% end %>

Or

views/products/index.html.erb

<% @products.each do |product| %>
  <% cache "product-#{product.id}" do %>
    <%= product.title %>
  <% end %>
<% end %>

If you pass an object to the cache method, it will take its id automatically, append a timestamp and generate a proper cache key (which is an MD5 hash). The cache will automatically expire if the product was updated.

In the console you should see an output similar to this one:

Write fragment views/products/12-20160413131556164995/0b057ac0a9b2a20d07f312c2f31bde45

This code can be simplified using the render method with the cached option:

views/products/index.html.erb

<%= render @products, cached: true %>

You can go further and apply so-called Russian doll caching:

views/products/index.html.erb

<% cache "products" do %>
  <%= render @products, cached: true %>
<% end %>

There are also cache_if and cache_unless methods that are pretty self-explainatory.

If you wish to expire your cache manually, use the expire_fragment method and pass the cache key to it:

@product = Product.find(params[:id])
# do something...
expire_fragment(@product)

Model Caching

Model caching (aka low level caching) is often used to cache a particular query, however, this solution can be employed to store any data. This functionality is also a part of Rails’ core.

Suppose we want to cache a list of our products fetched inside the index action. Introduce a new class method for the Product:

models/product.rb

[...]
class << self
  def all_cached
    Rails.cache.fetch("products") { Product.all }
  end
end
[...]

The fetch method can both read and write Rails’ cache (the first argument is the storage’s name). If the requested storage is empty, it will be populated with the content specified inside the block. If it contains something, the result will simply be returned. There are also write and read methods available.

In this example, we create a new storage called “products” that is going to contain a list of our products. Now use this new method inside the controller’s action:

products_controller.rb

[...]
def index
  @products = Product.all_cached
end
[...]

Of course, you’ll need to implement cache expiration logic. For simplicity, let’s do it inside the after_commit callback:

models/product.rb

[...]
after_commit :flush_cache
[...]
private

def flush_cache
  Rails.cache.delete('products')
end
[...]

After any product is updated, we invalidate the “products” cache.

Model caching is pretty simple to implement and can significantly speed up your complex queries.

HTTP Caching

The last type of caching we are going to discuss today is HTTP caching that relies on HTTP_IF_NONE_MATCH and HTTP_IF_MODIFIED_SINCE headers. Basically, these headers are being sent by the client to check when the page’s content was last modified and whether its unique id has changed. This unique id is called an ETag and is generated by the server.

The client receives an ETag and sends it inside the HTTP_IF_NONE_MATCH header on subsequent requests. If the ETag sent by the client does not match the one generated on the server, it means the page has been modified and needs to be downloaded again. Otherwise, a 304 (“not modified”) status code is returned and a browser uses a cached copy of the page. To learn more, I highly recommend reading this article by Google that provides very simple explanations.

There are two methods that can be used to implement HTTP caching: stale? and fresh_when. Suppose we want to cache a show page:

products_controller.rb

[...]
def show
  @product = Product.find(params[:id])
end
[...]

views/products/show.html.erb

<h1>Product <%= @product.title %></h1>

config/routes.rb

[...]
resources :products, only: [:index, :show]
[...]

Use the stale? method and set the :last_modified and :etag options:

products_controller.rb

[...]
def show
  @product = Product.find(params[:id])
  if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key)
    respond_to do |format|
      format.html
    end
  end
end
[...]

The idea is simple – if the product was recently updated, the cache will be invalidated and the client will have to download the page once again. Otherwise, Rails will send a 304 status code. cache_key is a special method that generates a proper unique id for a record.

You may further simplify the code

products_controller.rb

[...]
def show
  @product = Product.find(params[:id])
  if stale?(@product)
    respond_to do |format|
      format.html
    end
  end
end
[...]

stale? will fetch the product’s timestamp and cache key automatically. fresh_when is a simpler method that can be used if you don’t want to utilize respond_to:

products_controller.rb

[...]
def show
  @product = Product.find(params[:id])
  fresh_when @product
end
[...]

It also accepts :last_modified and :etag options, just like stale?.

HTTP caching may be hard to implement, especially for complex pages, but having it in place can really boost web site’s performance.

Conclusion

In this article we’ve discussed various caching techniques that you can employ in your Rails apps. Remember, however, that preliminary optimization is evil, so you should assess which caching solution suits you before implementing it.

Which caching techniques do you use in your apps? Share your experience in the comments!

Frequently Asked Questions (FAQs) about Caching in Rails

What is the importance of caching in Rails?

Caching is a crucial aspect of Rails that significantly enhances the performance of a Rails application. It does this by storing the result of an expensive or time-consuming operation, so that when the same operation is needed again, it can be retrieved from the cache instead of being computed from scratch. This reduces the load on the server and speeds up the response time, leading to a smoother and faster user experience.

How does Rails caching work?

Rails caching works by storing the result of a computation in a cache store. When the same computation is needed again, Rails first checks the cache store. If the result is found, it is returned immediately. If not, the computation is performed, the result is stored in the cache for future use, and then the result is returned. This process is transparent to the user and greatly speeds up the performance of the application.

What are the different types of caching in Rails?

Rails supports several types of caching, including page caching, action caching, and fragment caching. Page caching is the fastest but also the most primitive, as it caches the entire content of a page. Action caching is similar to page caching but allows for before filters. Fragment caching is the most flexible, as it allows for caching of individual view fragments.

How can I implement caching in my Rails application?

Implementing caching in a Rails application involves configuring the cache store, enabling caching in the environment configuration, and then using the cache methods in your views, controllers, and models. The exact steps will depend on the type of caching you want to implement and the specifics of your application.

What are the potential pitfalls of caching in Rails?

While caching can greatly improve performance, it can also introduce complexity and potential issues. For example, you need to ensure that your cache is always up-to-date and that stale data is not served to the user. You also need to manage the size of your cache to prevent it from consuming too much memory.

How can I test the effectiveness of caching in my Rails application?

You can test the effectiveness of caching in your Rails application by using benchmarking tools such as Rails’ built-in benchmark method or third-party tools like New Relic. These tools can help you measure the response time of your application with and without caching, so you can see the impact of your caching strategies.

Can I use caching with a Rails API?

Yes, you can use caching with a Rails API. In fact, caching can be particularly beneficial for APIs, as it can significantly reduce the load on the server and improve response times.

How can I clear the cache in Rails?

You can clear the cache in Rails by using the Rails.cache.clear method. However, be aware that this will clear the entire cache, so use it judiciously.

What is Russian Doll caching in Rails?

Russian Doll caching is a caching strategy in Rails where caches are nested within each other, like Russian dolls. This allows for maximum reuse of cached content and can greatly improve performance.

Can I use caching with Rails and React?

Yes, you can use caching with Rails and React. In fact, caching can be particularly beneficial in this context, as it can help to speed up the rendering of React components.

Ilya Bodrov-KrukowskiIlya Bodrov-Krukowski
View Author

Ilya Bodrov is personal IT teacher, a senior engineer working at Campaigner LLC, author and teaching assistant at Sitepoint and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails) and JavaScript. He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.

cachingGlennGRuby on Rails
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week