14.3. Page Caching

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 to have 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 below 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 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.

Warning

When programming your Rails application, please ensure that you also update this page itself, or delete it! You will find a description in the section called “Deleting Page Caches Automatically”. Otherwise, you 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://0.0.0.0:3000/companies?search=abc this automatically becomes http://0.0.0.0:3000/companies. But that can easily be fixed with a better route logic.
Please install a fresh example application (see the section called “A Simple Example Application”).

Activating Page Caching in Development Mode

First we need to go to the file config/environments/development.rb and set the item config.action_controller.perform_caching to true:
config.action_controller.perform_caching = true
Otherwise, we cannot try the page caching in development mode. In production mode, page caching is enabled by default.

Caching Company Index and Show View

Enabling page caching happens in the controller. If we want to cache the index and show views for Company, we need to go to the controller app/controllers/companies_controller.rb and enter the command caches_page :index, :show at the top:
class CompaniesController < ApplicationController
  caches_page :index, :show

  # GET /companies
  # GET /companies.json
  def index
    @companies = Company.all

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @companies }
    end
  end

[...]
Before starting the application, the public directory looks like this:
public/
|-- 404.html
|-- 422.html
|-- 500.html
|-- favicon.ico
|-- index.html
`-- robots.txt
After starting the appliation with rails server and going to the URLs http://0.0.0.0:3000/companies and http://0.0.0.0:3000/companies/1 via a web browser, it looks like this:
public/
|-- 404.html
|-- 422.html
|-- 500.html
|-- companies
|   `-- 1.html
|-- companies.html
|-- favicon.ico
|-- index.html
`-- robots.txt
The files public/companies.html and public/companies/1.html have 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 cache, you should also cache directly zipped gz files. You can do this via the option :gzip => true or use a specific compression parameter as symbol instead of true (for example :best_compression).
The controller app/controllers/companies_controller.rb would then look like this at the beginning:
class CompaniesController < ApplicationController
  caches_page :index, :show, :gzip => :best_compression

  # GET /companies
  # GET /companies.json
  def index
    @companies = Company.all

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @companies }
    end
  end

[...]
This automatically saves a compressed and an uncompressed version of each page cache:
public/
|-- 404.html
|-- 422.html
|-- 500.html
|-- companies
|   |-- 1.html
|   `-- 1.html.gz
|-- companies.html
|-- companies.html.gz
|-- favicon.ico
|-- index.html
`-- robots.txt

The File Extension .html

Rails saves the page accessed at http://0.0.0.0:3000/companies under the file name companies.html. So the upstream web server will find and deliver this file if you go to http://0.0.0.0:3000/companies.html, but not if you try to go to http://0.0.0.0:3000/companies, because the extension .html at the end of the URI is missing.
If you are using the Nginx server as described in Chapter 15, Web Server in Production Mode, the easiest way is adapting 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 URI 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 am going to show you an approach that does not use a sweeper, but works directly in the model with ActiveRecord callbacks.
In our phone book application, we always need to delete the cache for http://0.0.0.0:3000/companies and http://0.0.0.0:3000/companies/company_id when editing a company. When editing an employee, we also have to delete the corresponding cache for the relevant employee.

Important

We need to ensure that we do not cache pages with a flash message. Nor does it make sense to integrate a CSRF metatag in these cached pages. In the following code, we take care of both these issues.

Controllers

Let's start with the controllers. Please edit the beginning of app/controllers/companies_controller.rb as follows:
class CompaniesController < ApplicationController
  caches_page :index, :show, :gzip => :best_compression, 
                             :if => Proc.new { flash.count == 0 }
  before_filter(only: [:index, :show]) { @page_caching_is_active = true if flash.count == 0 }

  # GET /companies
  # GET /companies.json
  def index
    @companies = Company.all

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @companies }
    end
  end

[...]
Please also insert the cache_page instruction into the controller app/controllers/employees_controller.rb:
class EmployeesController < ApplicationController
  caches_page :index, :show, :gzip => :best_compression, 
                             :if => Proc.new { flash.count == 0 }
  before_filter(only: [:index, :show]) { @page_caching_is_active = true if flash.count == 0 }

  # GET /employees
  # GET /employees.json
  def index
    @employees = Employee.all

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @employees }
    end
  end

[...]

Models

Now we still need to fix the models so that the corresponding caches are deleted automatically as soon as an object is created, edited or deleted.
app/models/company.rb
class Company < ActiveRecord::Base
  attr_accessible :name

  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_helpers.company_path(self))
    ActionController::Base.expire_page(Rails.application.routes.url_helpers.companies_path)
  end

end
app/models/employee.rb
class Employee < ActiveRecord::Base
  attr_accessible :company_id, :first_name, :last_name, :phone_number

  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_helpers.employee_path(self))
    ActionController::Base.expire_page(Rails.application.routes.url_helpers.employees_path)
    self.company.expire_cache
  end

end

application.html.erb

In the app/views/layouts/application.html.erb we still need to check if a CSRF metatag is present. This is not the case with cached pages.
<!DOCTYPE html>
<html>
<head>
  <title>PhoneBook</title>
  <%= stylesheet_link_tag    "application", :media => "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tag unless @page_caching_is_active %>
</head>
<body>

<%= yield %>

</body>
</html>

Updates about this book will be published on my Twitter feed.