homeASCIIcasts

169: Dynamic Page Caching 

(view original Railscast)

Other translations: Cn It

The application we’ll be working with in this episode is a forum app for piano players. The page below is the home page and it’s currently taking a couple of seconds to load.

The home page of our site takes a few seconds to load.

While I could tell you that the page is taking so long to load because it’s so complicated, with a whole load of stuff going on behind the scenes, the truth is that the delay is simulated by adding a two-second sleep to the request. For the purposes of this demonstration we’ll assume that the slowness is real and, given that this is the site’s home page, we want it to load as quickly as possible. To improve the loading time we’ll optimize the performance of the page as much as we can.

Page Caching

The best way to improve the performance of a page in a Rails app is to cache it. (Page caching was covered back in episode 89). Let’s turn on page caching and see how we get on.

Caching is turned off by default in development mode so we’ll have to open /config/environments/development.rb and enable it by setting config.action_controller.perform_caching to true. That done we can add caching to the index action of our forums controller.

class ForumsController < ApplicationController
  before_filter :admin_required, :except => [:index, :show]
  caches_page :index
  def index
    @forums = Forum.all
    sleep 2
  end
  # other methods omitted.
end

We’ll need to stop and start our server for the configuration change we made to be picked up. We can then reload our home page. It should take a couple of seconds to load the first time, but load almost instantly for each subsequent refresh as the page is served from the cache.

Caching works by generating static HTML files in the /public directory. When a request is made to a Rails application, the server the app is running under first looks for a static file that matches the request. For example, for this page it will look for a file called /public/episodes/169-dynamic-page-caching.html. If the server finds the file it will return it and the Rails application will know nothing about the request, but it can’t find it file it will pass the request on to Rails to process. If the action requested has caching enabled then Rails will, after processing the request, write the static HTML file in the appropriate place under the /public directory so that the server can serve it directly the next time the same request is made. (As an aside, this is the reason you need to remove the static index.html page that is created when you generate a new Rails application. It acts in the same way a cached file does and stops the real root action from being requested.)

The cached page can have no dynamic content in it at all, which is a problem for our complex page as it contains content that changes depending on whether the current user is logged in. If we cache the whole page as it is now then anyone who visits it will appear to be logged in as eifion.

<div id="user_nav">
  <p>Welcome <strong>eifion</strong>! Not you? <a href="/logout">Logout</a></p>
</div>

Even if we click the “log out” link we’ll still appear to be logged in because the logout page redirects back to the home page after logging us out so we’ll be served the same static page.

It seems like there’s too much dynamic content on this page to allow us to use page caching. We could look at using fragment caching to cache the part of the page that shows the list of forums but even here there is dynamic content as the “edit” and “destroy” links for each forum are only shown to users that are admins.

Using JavaScript with Page Caching

While it might seem that our home page can’t be cached there is a way to do it. We can remove the dynamic content from the page, use page caching to cache the basic page and then add the dynamic content back via JavaScript.

Before we start we’ll temporarily disable the caching and the sleep in our forums controller. We’ll also need to remove the cached index page or we won’t see any of the changes we make.

rm public/index.html

The first parts of the page we’ll make dynamic are the links that allow admin users to edit or delete forums and the “new forum” link at the bottom of the page. The view code is shown below with the two parts of the page that are only shown to admins wrapped in if statements.

<% title "Piano Forums" %>
<div id="forums">
  <% for forum in @forums %>
    <div class="forum">
      <h2><%= link_to h(forum.name), forum %></h2>
      <p><%= h forum.description %></p>
      <% if admin? %>
        <p class="admin">
          <%= link_to "Edit", edit_forum_path(forum) %> |
          <%= link_to "Destroy", forum, :confirm => 'Are you sure?', :method => :delete %>
        </p>
      <% end %>
    </div>
  <% end %>
</div>
<% if admin? %>
  <p class="admin"><%= link_to "New Forum", new_forum_path %></p>
<% end %>

We’ll remove the if statements and hide the links with CSS instead.

<p class="admin" style="display:none;">
  <%= link_to "Edit", edit_forum_path(forum) %> |
  <%= link_to "Destroy", forum, :confirm => 'Are you sure?', :method => :delete %>
</p>
<p class="admin" style="display:none;"><%= link_to "New Forum", new_forum_path %></p>

If we reload the page now, the admin links are on the page but are hidden.

The edit, destroy and new links are now hidden.

Now we’ll use JavaScript to make the links visible again if the user is an admin. At the top of the view we’ll call a helper method that will add references to two JavaScript files into the HEAD section of the page. (This helper method is part of Ryan Bates’ nifty layout generator). The first JavaScript file we’ll reference is the jQuery library, although you can use Prototype if you prefer; the second is a reference to an action in the UsersController.

<% javascript 'jquery', '/users/current' %>

When the page loads the code above will generate the following HTML.

<script src="/javascripts/jquery.js" type="text/javascript"></script>
<script src="/users/current.js" type="text/javascript"></script>

The first line loads the jQuery library and we’ll need to download the it and add it to our application’s /public/javascripts directory. The second one calls the show action of the users controller, with current passed as a dummy id. We’ll have to modify the show action so that it can respond to JavaScript requests.

The controller will need a show method, but this can be left blank.

def show
end

Next we’ll need a new view file so that the controller can respond to JavaScript requests. In the /app/views/users directory we’ll create a file called show.js.erb and put the following code in to it.

$(document).ready(function () {
  <% if admin? %>
  $('.admin').show();
  <% end %>
});

The code above uses jQuery’s $(document).ready() function to run JavaScript code when the document’s object model has loaded. The code will be empty unless the user is an admin, but if they are then all of the page’s elements that have a class of admin will be shown.

Updating The Status

The part of the page that shows the user’s status will also need to be generated dynamically, as will the flash messages that show when a users logs in or out. Both of these parts of the page are in the application’s layout file.

<%- flash.each do |name, msg| -%>
  <%= content_tag :div, msg, :id => "flash_#{name}" %>
<%- end -%>
<div id="user_status">
  <% if current_user %>
    <p>Welcome <strong><%= current_user.username %></strong>! Not you? <%= link_to "Logout", logout_path %></p>
  <% else %>
    <%= link_to "Register", new_user_path %>
    <%= link_to "Log in", login_path %>
  <% end %>
</div>

Updating these parts of the page with JavaScript will be easier if they are in a partial so we’ll move the code above into a partial file at /app/views/layouts/dynamic_header.html.erb. We can then replace the code above with a reference to our new partial.

<%= render 'layouts/dynamic_header' %>

What we need to do now is remove the contents of the partial from our static, cached home page and add them back with JavaScript. For this we need to pass a message to the partial to tell it not to render itself on the home page. One slightly hacky way to do this is to add an instance variable in the view code of the home page. The layout is rendered after the view, so the variable will be available in the layout.

At the top of /app/views/forums/index.html.erb we’ll add a variable called hide_dynamic and set it to true.

<% title "Piano Forums" %>
<% javascript 'jquery', '/users/current' %>
<% @hide_dynamic = true %>
<div id="forums">
<!-- rest of page... -->

We can then modify the call to the dynamic_header layout so that it isn’t rendered if @hide_dynamic is true.

<%= render :partial => 'layouts/dynamic_header' unless @hide_dynamic %>

If we refresh the page now we’ll see that the user’s status and the log out links have disappeared. We’ll now need to modify our JavaScript so that they can be added back.

The users status no longer appears.

We need to modify our jQuery function so that it will renders the partial to show the user’s current status and any flash messages. We only need to add one line of jQuery code to achieve this.

$(document).ready(function () {
  $('#container').prepend('<%= escape_javascript render("layouts/dynamic_header") %>');
  <% if admin? %>
  $('.admin').show();
  <% end %>
});

We want to add the partial as the first child of the div with the id of container, so we use jQuery’s prepend() function to add it there. This function takes a string of HTML as an argument so we can pass the rendered partial, wrapped in escape_javascript to ensure that the content of the partial doesn’t cause any JavaScript errors.

Now when we reload the page the user status panel will reappear, though as it’s added by the JavaScript call the static HTML of the page will now be the same whether there’s a logged-in user or not.

Re-enabling Caching

Now that we have all of the dynamic parts of the page updated by JavaScript we can turn the page’s caching back on and add the sleep back to simulate the page’s non-existent complexity.

When we refresh the page again, it will take a couple of seconds to load the first time, but subsequent refreshes will load almost instantly as the page is served from the cache. The difference now is that we can log in and out and the page will be updated to reflect our status, and any admin-only links are hidden and show as expected.

The dynamic parts of the page are now set by the JavaScript.

This approach won’t be as efficient as full-page caching as the JavaScript parts still hit our Rails application, but you should see a performance boost for complex files as most of the page is served as static HTML.