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.
Key Takeaways
- Caching, which means storing data to fulfill subsequent requests faster, can significantly speed up a web application and improve its user experience. The various caching techniques available in Rails include page, action, fragment, model, and HTTP caching.
- Page caching works by saving the whole HTML page to a file in the public directory, which is then sent directly to the user on subsequent requests. However, it has limited usage, especially for pages that look different for different users or for websites accessed only by authorized users.
- Fragment caching allows for caching only a part of the page. This functionality is built into Rails’ core and can be implemented using the cache method that accepts cache storage’s name. If an object is passed to the cache method, it will automatically take its id, append a timestamp, and generate a proper cache key.
- HTTP caching relies on HTTP_IF_NONE_MATCH and HTTP_IF_MODIFIED_SINCE headers. The client receives an ETag and sends it in 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, 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.
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
orunless
to instruct whether to cache the actionexpires_in
– time to expire cache automaticallycache_path
– path to store the cache. Useful for actions with multiple possible routes that should be cached differentlylayout
– if set tofalse
, will only cache the action’s contentformat
– 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 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.