Caching in Ruby on Rails 5.2

Stefan Wintermeyer
Ruby Inside
Published in
23 min readApr 30, 2018

--

This Medium post is a copy of Chapter 14 about caching from my “Learn Rails 5.2” book (Amazon Link, Apress Link) which was published in April 2018 by Apress. Please contact me in case you need consulting or training for Rails, Phoenix or WebPerformance.

Homepage: https://www.wintermeyer-consulting.de
Email:
sw@wintermeyer-consulting.de
Twitter:
@wintermeyer

Abstract

With the caching of web applications, most people tend to wait to implement it until they encounter performance problems. First the admin usually looks at the database and adds an index here and there. If that does not help, the admin then takes a look at the views and adds fragment caching. But this is not the best approach for working with caches.

The aim of this chapter is to help you understand how key-based cache expiration works. You can then use this approach to plan new applications already on the database structure level in such a way that you can cache optimally during development.

Cover of “Learn Rails 5.2” by Stefan Wintermeyer

With the caching of web applications, most people tend to wait to implement it until they encounter performance problems. First the admin usually looks at the database and adds an index here and there.

If that does not help, the admin then takes a look at the views and adds fragment caching. But this is not the best approach for working with caches. The aim of this chapter is to help you understand how key-based cache expiration works. You can then use this approach to plan new applications already on the database structure level in such a way that you can cache optimally during development.

There are two main arguments for using caching:

  • The application becomes faster for the user. A faster web page
    results in happier users, which results in a better conversion rate.
  • You need less hardware for the web server because you require less
    CPU and RAM resources for processing the queries.

If these two arguments are irrelevant for you, then there’s no need to
read this chapter.

I will cover three caching methods:

  • HTTP caching: This is the sledgehammer among the caching methods and
    the ultimate performance weapon. In particular, web pages that are
    intended for mobile devices should try to make the most of HTTP
    caching. If you use a combination of key-based cache expiration and
    HTTP caching, you save a huge amount of processing time on the server
    and also bandwidth.
  • Page caching: This is the screwdriver among the caching methods. You
    can get a lot of performance out of the system, but it is not as good
    as HTTP caching.
  • Fragment caching: This is the tweezers among the caching methods, so
    to speak. But do not underestimate it!

The aim is to optimally combine all three methods.

The Example Application

You will use a simple phone book with a company model and an employees
model.

Create the new Rails app, as shown here:

$ rails new phone_book
[…]
$ cd phone_book
$ rails generate scaffold company name
[…]
$ rails generate scaffold employee company:references \
last_name first_name phone_number
[…]
$ rails db:migrate
[…]

Models

Listing [3]14–1 and Listing [4]14–2 show the setup for the two models.

class Company < ApplicationRecord
validates :name,
presence: true,
uniqueness: true
has_many :employees, dependent: :destroy
def to_s
name
end
end
Listing 14–1 app/models/company.rbclass Employee < ApplicationRecord
belongs_to :company, touch: true
validates :first_name,
presence: true
validates :last_name,
presence: true
validates :company,
presence: true
def to_s
“#{first_name} #{last_name}”
end
end
Listing 14–2 app/models/employee.rb

Views

Go ahead and change the two company views, shown in Listing 14–3 and
Listing 14–4, to list the number of employees in the index view and
all the employees in the show view.

[…]
<table>
<thead>
<tr>
<th>Name</th>
<th>Number of employees</th>
<th colspan=”3"></th>
</tr>
</thead>
<tbody>
<% @companies.each do |company| %>
<tr>
<td><%= company.name %></td>
<td><%= company.employees.count %></td>
[…]
</tr>
<% end %>
</tbody>
</table>
[…]
Listing 14–3 app/views/companies/index.html.erb<p id=”notice”><%= notice %></p>
<p>
<strong>Name:</strong>
<%= @company.name %>
</p>
<% if @company.employees.any? %>
<h1>Employees</h1>
<table>
<thead>
<tr>
<th>Last name</th>
<th>First name</th>
<th>Phone number</th>
</tr>
</thead>
<tbody>
<% @company.employees.each do |employee| %>
<tr>
<td><%= employee.last_name %></td>
<td><%= employee.first_name %></td>
<td><%= employee.phone_number %></td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
<%= link_to ‘Edit’, edit_company_path(@company) %> |
<%= link_to ‘Back’, companies_path %>
Listing 14–4 app/views/companies/show.html.erb

If you like this post I’d like to ask you for a favour:
Create an account at my open-source business network https://www.vutuv.de

Thank you and see you there!

Example Data

To easily populate the database, you can use the Faker gem (see
http://faker.rubyforge.org/). With Faker, you can generate random
names and phone numbers. Please add the line shown in Listing 14–5
in the Gemfile.

[…]
gem ‘faker’
[…]
Listing 14–5 Gemfile

Then start bundle, as shown here:

$ bundle

With db/seeds.rb, you can create 30 companies with a random number of
employees in each case, as shown in Listing 14–6.

30.times do
company = Company.new(:name => Faker::Company.name)
if company.save
SecureRandom.random_number(100).times do
company.employees.create(
first_name: Faker::Name.first_name,
last_name: Faker::Name.last_name,
phone_number: Faker::PhoneNumber.phone_number
)
end
end
end
Listing 14–6 db/seeds.rb

You can populate it via rails db:seed.

$ rails db:seed

You can start the application with rails server and retrieve the
example data with a web browser by going to the URL
http://localhost:3000/companies or http://localhost:3000/companies/1.

Normal Speed of the Pages to Optimize

In this chapter, you will optimize the example web pages. Start the
Rails application in development mode with rails server. (The relevant
time values, of course, depend on the hardware you are using.)

$ rails server

To access the web pages, use the command-line tool curl
(http://curl.haxx.se/). Of course, you can also access the web
pages with other web browsers. You can look at the time shown in the
Rails log for creating the page. In reality, you need to add the time
it takes for the page to be delivered to the web browser.

List of All Companies (Index View)

At the URL http://localhost:3000/companies, the user can see a list of
all the saved companies with the relevant number of employees.

Generating the page takes 89ms on my machine.

Completed 200 OK in 89ms (Views: 79.0ms | ActiveRecord: 9.6ms)

Detailed View of a Single Company (Show View)

At the URL http://localhost:3000/companies/1, the user can see the
details of the first company with all the employees.

Generating the page takes 51ms on my machine.

Completed 200 OK in 51ms (Views: 48.9ms | ActiveRecord: 0.9ms)

HTTP Caching

HTTP caching attempts to reuse already loaded web pages or files. For
example, if you visit a web page such as www.nytimes.com or
www.wired.com several times a day to read the latest news, then
certain elements of that page (for example, the logo at the top of the
page) will not be loaded again from the server on your second visit.
Your browser already has these files in the local cache, which saves
the loading time and bandwidth.

Within the Rails framework, your aim is to answer the question “Has a
page changed?” in the controller. Normally, most of the time is spent
on rendering the page in the view. I’d like to repeat that: most of the
time is spent on rendering the page in the view!

Last-Modified

The web browser knows when it has downloaded a resource (e.g., a web
page) and then placed it into its cache. On a second request, it can
pass this information to the web server in an If-Modified-Since:
header. The web server can then compare this information to the
corresponding file and either deliver a newer version or return an HTTP
304 Not Modified code as response. In the case of a 304, the web
browser delivers the locally cached version. Now you are going to say,
“That’s all very well for images, but it won’t help me at all for
dynamically generated web pages such as the index view of the
companies.” However, you are underestimating the power of Rails.
[Para, Type = Important, ID = Par41]

Please modify the times used in the examples in accordance with your own
circumstances.

Go ahead and edit the show method in the controller file
app/controllers/companies_controller.rb, as shown in Listing 14–7.

# GET /companies/1
# GET /companies/1.json
def show
fresh_when last_modified: @company.updated_at
end
Listing 14–7 app/controllers/companies_controller.rb

After restarting the Rails application, take a look at the HTTP header
of http://localhost:3000/companies/1, as shown here:

$ curl -I http://localhost:3000/companies/1
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Last-Modified: Sat, 27 Jan 2018 18:38:05 GMT
[…]

The Last-Modified entry in the HTTP header was generated by fresh_when
in the controller. If you later go to the same web page and specify
this time as well, then you do not get the web page back; you get a 304
Not Modified message, as shown here:

$ curl -I http://localhost:3000/companies/1 — header
‘If-Modified-Since: Sat, 27 Jan 2018 18:38:05 GMT’
HTTP/1.1 304 Not Modified
[…]

In the Rails log, you will find this:

Started HEAD “/companies/1” for 127.0.0.1 at 2018–01–27 18:24:21 +0100
Processing by CompaniesController#show as */*
Parameters: {“id”=>”1"}
Company Load (0.1ms) SELECT “companies”.* FROM “companies” WHERE
“companies”.”id” = ? LIMIT ? [[“id”, 1], [“LIMIT”, 1]]
Completed 304 Not Modified in 2ms (ActiveRecord: 0.1ms)

It took Rails 2ms on my machine to answer this request, compared to the
51ms of the standard variation. This is much faster! So, you have used
fewer resources on the server and saved a massive amount of bandwidth.
The user will be able to see the page much more quickly.

etag

Sometimes the update_at field of a particular object is not meaningful
on its own. For example, if you have a web page where users can log in
and this page then generates web page contents based on a role model,
it can happen that user A as the admin is able to see an Edit link that is not displayed to user B as a normal user. In such a scenario, the Last-Modified header explained earlier does not help. Actually, it would do harm.

In these cases, you can use the etag header. The etag is generated by the web server and delivered when the web page is first visited. If the user visits the same URL again, the browser can then check whether the corresponding web page has changed by sending an If-None-Match: query to the web server.

Please edit the index and show methods in the controller file
app/controllers/companies_controller.rb, as shown in Listing 14–8.

# GET /companies
# GET /companies.json
def index
@companies = Company.all
fresh_when etag: @companies
end
# GET /companies/1
# GET /companies/1.json
def show
fresh_when etag: @company
end
Listing 14–8 app/controllers/companies_controller.rb

A special Rails feature comes into play for the etag: Rails automatically sets a new CSRF token for each new visitor of the web site. This prevents cross-site request forgery attacks (see http://wikipedia.org/wiki/Cross_site_request_forgery). But it also means that each new user of a web page gets a new etag for the same page. To ensure that the same users also get identical CSRF tokens, these are stored in a cookie by the web browser and consequently sent back to the web server every time the web page is visited. You have to tell curl that you want to save all cookies in a file and transmit these cookies later if a request is received.

For saving, you use the -c cookies.txt parameter.

$ curl -I http://localhost:3000/companies -c cookies.txt
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
ETag: W/”53830a75ef520df8ad8e1894cf1e5003"
[…]

With the parameter -b cookies.txt, curl sends these cookies to the web server when a request arrives. Now you get the same etag for two subsequent requests.

$ curl -I http://localhost:3000/companies -b cookies.txt
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
ETag: W/”53830a75ef520df8ad8e1894cf1e5003"
[…]
$ curl -I http://localhost:3000/companies -b cookies.txt
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
ETag: W/”53830a75ef520df8ad8e1894cf1e5003"
[…]

You now use this etag to find out in the request with If-None-Match if
the version you have cached is still up-to-date.

$ curl -I http://localhost:3000/companies -b cookies.txt — header
‘If-None-Match: W/”53830a75ef520df8ad8e1894cf1e5003"’
HTTP/1.1 304 Not Modified
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
ETag: W/”53830a75ef520df8ad8e1894cf1e5003"
[…]

You get a 304 Not Modified in response. Let’s look at the Rails log.

Started HEAD “/companies” for 127.0.0.1 at 2018–01–27 18:36:25 +0100
Processing by CompaniesController#index as */*
(0.2ms) SELECT COUNT(*) AS “size”, MAX(“companies”.”updated_at”) AS
timestamp FROM “companies”
Completed 304 Not Modified in 24ms (ActiveRecord: 0.2ms)

Rails took only 24ms on my machine to process the request. Plus, you have saved bandwidth again. The user will be happy with the speedy web application.

Find more generic information about etag headers at
https://en.wikipedia.org/wiki/HTTP_ETag.

current_user and Other Potential Parameters

As the basis for generating an etag, you can pass not just an object but also an array of objects. This way, you can solve the problem with the logged-in user who might get different content than a non-logged-in user. Let’s assume that a logged-in user is output with the method current_user.

You have to add etag { current_user.try :id } in app/controllers/application_controller.rb to make sure that all etags
in the application include the current_user.id value, which is nil if nobody is logged in, as shown in Listing 14–9.

class ApplicationController < ActionController::Base
etag { current_user.try :id }
end
Listing 14–9 app/controllers/application_controller.rb

You can chain other objects in this array too and use this approach to
define when a page has not changed.

The Magic of touch

What happens if an employee is edited or deleted? Then the show view
and potentially the index view would have to change as well. That is
the reason for the following line in the employee model:

belongs_to :company, touch: true

Every time an object of the class Employee is saved in edited form and
if touch: true is used, ActiveRecord updates the superordinate Company
element in the database. The updated_at field is set to the current
time. In other words, it is “touched.”

This approach ensures that the correct content is delivered.

stale?

Up to now, I was assuming that only HTML pages are being delivered. So,
I showed how to use fresh_when and then do without the respond_to do
|format| block. But HTTP caching is not limited to HTML pages. What if
you want to render JSON, for example, as well and want to deliver it
via HTTP caching? You need to use the method stale?. Using stale?
resembles using the method fresh_when. Here’s an example:

def show
if stale? @company
respond_to do |format|
format.html
format.json { render json: @company }
end
end
end

Using Proxies (public)

I have also been assuming you were using a cache on the web browser.
But on the Internet, there are many proxies that are often closer to
the user and can therefore be useful for caching in the case of
nonpersonalized pages. If the example is a publicly accessible phone
book, then you can activate the free services of the proxies with the
parameter public: true in fresh_when or with stale?.

Here’s an example:

# GET /companies/1
# GET /companies/1.json
def show
fresh_when @company, public: true
end

You can go to the web page and get the output, as shown here:

$ curl -I http://localhost:3000/companies/1
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
ETag: W/”f37a06dbe0ee1b4a2aee85c1c326b737"
Last-Modified: Sat, 27 Jan 2018 17:16:53 GMT
Content-Type: text/html; charset=utf-8
Cache-Control: public
[…]

The header Cache-Control: public tells all proxies that they can also cache this web page.

Using proxies always has to be done with great caution. On the one hand, they are brilliantly suited for delivering your own web page quickly to more
users, but on the other hand, you have to be absolutely sure that no personalized pages are cached on public proxies. For example, CSRF tags
and flash messages should never end up in a public proxy. For CSRF tags, it is a good idea to make the output of csrf_meta_tag in the default app/views/layouts/application.html.erb layout dependent on the
question of whether the page may be cached publicly, as shown here:

<%= csrf_meta_tag unless response.cache_control[:public] %>

Cache-Control with Time Limit

When using etag and Last-Modified, you can assume that the web browser
definitely checks once more with the web server if the cached version of a web page is still current. This is a very safe approach.

But you can take the optimization one step further by predicting the future: if you am already sure when delivering the web page that this web page is not going to change in the next two minutes, hours, or days, then you can tell the web browser this directly. It then does not need to check back again within this specified period of time. This overhead savings has advantages, especially with mobile web browsers with relatively high latency. Plus, you save server load on the web server.

In the output of the HTTP header, you may already have noticed the corresponding line in the etag and Last-Modified examples, shown here:

Cache-Control: max-age=0, private, must-revalidate

The item must-revalidate tells the web browser that it should
definitely check back with the web server to see whether a web page has
changed in the meantime. The second parameter, private, means that only
the web browser is allowed to cache this page. Any proxies on the way are not permitted to cache this page.

If you decide for the phone book that the web page is going to stay unchanged for at least two minutes, then you can expand the code example by adding the method expires_in. The controller app/controllers/companies.rb will then contain the following code for the method show:

# GET /companies/1
# GET /companies/1.json
def show
expires_in 2.minutes
fresh_when @company, public: true
end

Now you get a different cache control information in response to a request.

$ curl -I http://localhost:3000/companies/1
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Date: Sat, 27 Jan 2018 17:58:56 GMT
ETag: W/”f37a06dbe0ee1b4a2aee85c1c326b737"
Last-Modified: Sat, 27 Jan 2018 17:16:53 GMT
Content-Type: text/html; charset=utf-8
Cache-Control: max-age=120, public
[…]

The two minutes are specified in seconds (max-age=120), and you no longer need must-revalidate. So, in the next 120 seconds, the web browser does not need to check back with the web server to see whether the content of this page has changed.

This mechanism is also used by the asset pipeline. Assets created there in the Production environment can be identified clearly by the checksum in the file name and can be cached for a long time both in the web browser and in public proxies. That’s why you have the following section in the Nginx configuration file:

location ^~ /assets/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}

Fragment Caching

With fragment caching, you can cache individual parts of a view. You can safely use it in combination with HTTP caching and page caching.
The advantages, once again, are a reduction of server load and faster web page generation, which means increased usability.

Please create a new example application (see “The Example Application”).

Enabling Fragment Caching in Development Mode

Fragment caching is by default disabled in the Development environment.
You can activate it with the command rails dev:cache, which touches the
file tmp/caching-dev.txt.

$ rails dev:cache
Development mode is now being cached.

To deactivate caching, run the same command again (this will delete the
file tmp/caching-dev.txt).

$ rails dev:cache
Development mode is no longer being cached.

In production mode, fragment caching is enabled by default.

Caching the Table of the Index View

On the page http://localhost:3000/companies, a computationally intensive table with all the companies is rendered. You can cache this table as a whole. To do so, you need to enclose the table in a <% cache(‘name_of_cache’) do %> … <% end %> block.

<% cache(‘name_of_cache’) do %>
[…]
<% end %>

Please edit the file app/views/companies/index.html.erb as shown in
Listing 14–10.

<h1>Companies</h1>
<% cache(‘table_of_all_companies’) do %>
<table>
<thead>
<tr>
<th>Name</th>
<th>Number of employees</th>
<th colspan=”3"></th>
</tr>
</thead>
<tbody>
<% @companies.each do |company| %>
<tr>
<td><%= company.name %></td>
<td><%= company.employees.count %></td>
<td><%= link_to ‘Show’, company %></td>
<td><%= link_to ‘Edit’, edit_company_path(company) %></td>
<td><%= link_to ‘Destroy’, company, method: :delete, data: {
confirm:
‘Are you sure?’ } %></td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
<br />
<%= link_to ‘New Company’, new_company_path %>
Listing 14–10 app/views/companies/index.html.erb

Then you can start the Rails server with rails server and go to the URL
http://localhost:3000/companies.

The first time, a page that has a fragment cache is a little bit slower because the cache has to be written. The second time it is a lot of faster.

Deleting the Fragment Cache

With the method expire_fragment, you can clear specific fragment caches. Basically, you can build this idea into the model in the same way as shown in the section “Deleting Page Caches Automatically.”

The model file app/models/company.rb will look like Listing 14–11.

class Company < ActiveRecord::Base
validates :name,
presence: true,
uniqueness: true
has_many :employees, dependent: :destroy
after_create :expire_cache
after_update :expire_cache
before_destroy :expire_cache
def to_s
name
end
def expire_cache
ActionController::Base.new.expire_fragment(‘table_of_all_companies’
)
end
end
Listing 14–11 app/models/company.rb

Because the number of employees also has an effect on this table, you also have to expand the file app/models/employees.rb accordingly, as shown in Listing 14–12.

class Employee < ActiveRecord::Base
belongs_to :company, touch: true
validates :first_name,
presence: true
validates :last_name,
presence: true
validates :company,
presence: true
after_create :expire_cache
after_update :expire_cache
before_destroy :expire_cache
def to_s
“#{first_name} #{last_name}”
end
def expire_cache
ActionController::Base.new.expire_fragment(‘table_of_all_companies’
)
end
end
Listing 14–12 app/models/employees.rb

Deleting specific fragment caches often involves a lot of effort in terms of programming. First, you often miss things; second, in big projects it’s not easy to keep track of all the different cache names. Often it is easier to automatically create names via the method cache_key. These then expire automatically in the cache.

Auto-expiring Caches

Managing fragment caching is rather complex with the naming convention
used in the section “Caching the Table of the Index View.” On the one hand, you can be sure that the cache does not have any superfluous ballast if you have programmed neatly, but on the other, it does not really matter. A cache is structured in such a way that it deletes old and no longer required elements on its own. If you use a mechanism that gives a fragment cache a unique name, as in the asset pipeline, then you do not need to go to the trouble of deleting fragment caches.

Rails has you covered. And it is pretty easy to do.

Let’s edit the index view in the file app/views/companies/index.html.erb, as shown in Listing 14–13.

<h1>Companies</h1>
<% cache(@companies) do %>
<table>
<thead>
<tr>
<th>Name</th>
<th>Number of employees</th>
<th colspan=”3"></th>
</tr>
</thead>
<tbody>
<% @companies.each do |company| %>
<tr>
<td><%= company.name %></td>
<td><%= company.employees.count %></td>
<td><%= link_to ‘Show’, company %></td>
<td><%= link_to ‘Edit’, edit_company_path(company) %></td>
<td><%= link_to ‘Destroy’, company, method: :delete, data: {
confirm:
‘Are you sure?’ } %></td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
<br />
<%= link_to ‘New Company’, new_company_path %>
Listing 14–13 app/views/companies/index.html.erb

You ask Rails to generate a cache key for @companies and use it. If you want to see the name of that cache key in your log, you have to add config.action_controller.enable_fragment_cache_logging = true in the
file config/environments/development.rb.

There is no general answer to the question of how much detail you should use
fragment caching. Do some experimenting with it and then look in the log to see how long things take.

Russian Doll Caching

In the previous example, you created one fragment cache for the whole table of companies. If one company within that table changes, the whole table has to be re-rendered. Depending on the kind of data, that might take a lot of time.

The idea of Russian doll caching is that you cache not only the whole table but each row of the table too. So, when one row changes, just this row has to be rendered; all other rows can be fetched from the cache. When done well, this can save a lot of resources.

Please take a look at the updated example, as shown in Listing 14–14.

<h1>Companies</h1>
<% cache(@companies) do %>
<table>
<thead>
<tr>
<th>Name</th>
<th>Number of employees</th>
<th colspan=”3"></th>
</tr>
</thead>
<tbody>
<% @companies.each do |company| %>
<% cache(company) do %>
<tr>
<td><%= company.name %></td>
<td><%= company.employees.count %></td>
<td><%= link_to ‘Show’, company %></td>
<td><%= link_to ‘Edit’, edit_company_path(company) %></td>
<td><%= link_to ‘Destroy’, company, method: :delete, data: {
confirm: ‘Are you sure?’ } %></td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
<% end %>
<br />
<%= link_to ‘New Company’, new_company_path %>
Listing 14–14 app/views/companies/index.html.erb

Change the Code in the View Results in an Expired Cache

Rails tracks an MD5 sum of the view you use. So if you change the file
(e.g., app/views/companies/index.html.erb), the MD5 changes, and all
the old caches will expire.

Cache Store

The cache store manages the stored fragment caches. If not configured
otherwise, this is the Rails MemoryStore. This cache store is good for developing but less suitable for a production system because it acts
independently for each Ruby on Rails process. So, if you have several
Ruby on Rails processes running in parallel in the production system,
each process holds its own MemoryStore.

MemCacheStore

Most production systems use memcached ([23]http://memcached.org/) as a
cache store. To enable memcached as a cache store in your production system, you need to add the line shown in Listing [24]14–15 in the file config/environments/production.rb.

config.cache_store = :mem_cache_storeListing 14–15 config/environments/production.rb

The combination of appropriately used auto-expiring caches and memcached is an excellent recipe for a successful web page.

Other Cache Stores

In the official Rails documentation you will find a list of other cache
stores; see http://guides.rubyonrails.org/caching_with_rails.html#cache-stores.

Page Caching

Page caching was removed from the core of Rails 4.0, but it is still available as a gem, and it is powerful. To do page caching, you need a bit of knowledge to configure your web server (e.g., Nginx or Apache). Page caching is not for the faint-hearted.

With page caching, it’s all about placing a complete HTML page (in other words, the render result of a view) into a subdirectory of the public directory and having it delivered directly from there by the web server (for example, Nginx) whenever the web page is visited next. Additionally, you can also save a compressed .gz version of the HTML page there. A production web server will automatically deliver files under public itself and can also be configured so that any .gz files present are delivered directly.

In complex views, that may take 500ms or even more for rendering; the
amount of time you save is of course considerable. As a web page operator, you once more save valuable server resources and can service more visitors with the same hardware. The web page user profits from a faster delivery of the web page.

When programming your Rails application, please ensure that you also update this page or delete it! You will find a description of how to do this in the section “Deleting the Page Caches Automatically.” Otherwise, you will end up with an outdated cache later.

Please also ensure that page caching rejects all URL parameters by default. For example, if you try to go to: http://localhost:3000/companies?search=abc, this automatically becomes http://localhost:3000/companies. But that can easily be fixed with different route logic.

Please install a fresh example application (see the section “The Example Application”) and add the gem with the following line in Gemfile:

gem ‘actionpack-page_caching’

Now install it with the command bundle install.

$ bundle install
[…]

Lastly, you have to tell Rails where to store the cache files. Please add the line shown in Listing 14–16 in your config/application.rb file.

config.action_controller.page_cache_directory =
“#{Rails.root.to_s}/public/deploy”
Listing 14–16 config/application.rb

Activating Page Caching in Development Mode

First you need to go to the file config/environments/development.rb and
set the item config.action_controller.perform_caching to true, as shown in Listing 14–17.

config.action_controller.perform_caching = trueListing 14–17 config/environments/development.rb

Otherwise, you cannot try page caching in development mode. In production mode, page caching is enabled by default.

Configure Your Web Server

Now you have to tell your web server (e.g., Nginx or Apache) that it should check the /public/deploy directory first before hitting the Rails application. You have to configure it so that it will deliver a .gz file if one is available.

There is no one perfect way of doing it. You have to find the best way of doing it in your environment on your own.

As a quick and dirty hack for development, you can set page_cache_directory
to public. Then your development system will deliver the cached page.

config.action_controller.page_cache_directory =
“#{Rails.root.to_s}/public”

Caching the Company Index and Show View

Enabling page caching happens in the controller. If you want to cache the show view for Company, you need to go to the controller
app/controllers/companies_controller.rb and enter the command caches_page :show at the top, as shown in Listing [28]14–18.

class CompaniesController < ApplicationController
caches_page :show
[…]
Listing 14–18 app/controllers/companies_controller.rb

Before starting the application, the public directory looks like this:

public/
+ — 404.html
+ — 422.html
+ — 500.html
+ — apple-touch-icon-precomposed.png
+ — apple-touch-icon.png
+ — favicon.ico
+ — robots.txt

After starting the application with rails server and going to the URLs
http://localhost:3000/companies and http://localhost:3000/companies/1
via a web browser, it looks like this:

public
+ — 404.html
+ — 422.html
+ — 500.html
+ — apple-touch-icon-precomposed.png
+ — apple-touch-icon.png
+ — deploy
| + — companies
| + — 1.html
+ — favicon.ico
+ — robots.txt

The file public/deploy/companies/1.html has been created by page caching.

From now on, the web server will only deliver the cached versions when
these pages are accessed.

gz Versions

If you use page caching, you should also cache directly zipped .gz files. You can do this via the option :gzip => true or use a specific compression parameter as a symbol instead of true (for example,
:best_compression).

The controller app/controllers/companies_controller.rb will look like Listing 14–19 at the beginning.

class CompaniesController < ApplicationController
caches_page :show, gzip: true
[…]
Listing 14–19 app/controllers/companies_controller.rb

This automatically saves a compressed version and an uncompressed
version of each page cache.

public
+ — 404.html
+ — 422.html
+ — 500.html
+ — apple-touch-icon-precomposed.png
+ — apple-touch-icon.png
+ — deploy
| + — companies
| + — 1.html
| + — 1.html.gz
+ — favicon.ico
+ — robots.txt

The File Extension .html

Rails saves the page accessed at http://localhost:3000/companies under the file name companies.html. So, the upstream web server will find and deliver this file if you go to http://localhost:3000/companies.html, but not if you try to go to http://localhost:3000/companies because the extension .html at the end of the URL is missing.

If you are using the Nginx server, the easiest way to do this is to adapt the try_files instruction in the Nginx configuration file as follows:

try_files $uri/index.html $uri $uri.html @unicorn;

Nginx then checks if a file with the extension .html of the currently
accessed URL exists.

Deleting Page Caches Automatically

As soon as the data used in the view changes, the saved cache files
have to be deleted. Otherwise, the cache would no longer be up-to-date.

According to the official Rails documentation, the solution for this problem is the class ActionController::Caching::Sweeper. But this approach, described at http://guides.rubyonrails.org/caching_with_rails.html#sweepers, has
a big disadvantage: it is limited to actions that happen within the controller. So, if an action is triggered via URL by the web browser, the corresponding cache is also changed or deleted. But if an object is deleted in the console, for example, the sweeper would not realize this. For that reason, I will show you an approach that does not use a sweeper but works directly in the model with ActiveRecord callbacks.

In the phone book application, you always need to delete the cache for http://localhost:3000/companies and http://localhost:3000/companies/company_id when editing a company. When editing an employee, you also have to delete the corresponding cache
for the relevant employee.

Models

You still need to fix the models so that the corresponding caches are deleted automatically as soon as an object is created, edited, or deleted, as shown in Listing 14–20 and Listing 14–21.

class Company < ActiveRecord::Base
validates :name,
presence: true,
uniqueness: true
has_many :employees, dependent: :destroy
after_create :expire_cache
after_update :expire_cache
before_destroy :expire_cache
def to_s
name
end
def expire_cache
ActionController::Base.expire_page(Rails.application.routes.url_hel
pers.company_path(self))
ActionController::Base.expire_page(Rails.application.routes.url_hel
pers.companies_path)
end
end
Listing 14–20 app/models/company.rbclass Employee < ActiveRecord::Base
belongs_to :company, touch: true
validates :first_name,
presence: true
validates :last_name,
presence: true
validates :company,
presence: true
after_create :expire_cache
after_update :expire_cache
before_destroy :expire_cache
def to_s
“#{first_name} #{last_name}”
end
def expire_cache
ActionController::Base.expire_page(Rails.application.routes.url_hel
pers.employee_path(self))
ActionController::Base.expire_page(Rails.application.routes.url_hel
pers.employees_path)
self.company.expire_cache
end
end
Listing 14–21 app/models/employee.rb

Preheating

Now that you have read your way through this chapter, here is a final tip: preheat your cache!

For example, if you have a web application in a company and you know that at 9 a.m. all employees are going to log in and then access this web application, then it’s a good idea to let your web server go through all those views a few hours in advance with a cron job. At night, your server is probably bored anyway.

Check out the behavior patterns of your users. With public web pages, this can be done, for example, via Google Analytics (www.google.com/analytics/). You will find that at certain times of the day, there is a lot more traffic going in. If you have a quiet phase prior to this, you can use it to warm up your cache.

The purpose of preheating is to save server resources and achieve better quality for the user because the web page is displayed more quickly.

Further Information

The best source of information on this topic is in the Rails
documentation at: http://guides.rubyonrails.org/caching_with_rails.html. There you can find additional information (e.g., low-level caching).

--

--

Stefan Wintermeyer
Ruby Inside

Ruby on Rails, Phoenix Framework, WebPerf and Photography. Father of two. German and English.