Recall that with page caching, Rails never gets involved. In some ways, that's a good thing, because you really can get excellent performance. Rails creates an HTML page once, places it in a directory, and forgets about it. From then on, the application server serves the pages, without a single cycle going to the application server. From a performance perspective, page caching is bliss.
I love page caching, and Rails makes it simple and clean. With a single line of code, you can enable the cache. With a few more lines of code, you can expire the cache by simply deleting a file, or by using a Rails higher-level API. Here's the problem. Not every site can use page caching. If you have any data on a page that changes at all based on who is looking at it, you can't page cache. And if you have any difficulty determining when you should expire a page, you may find page caching demanding.
For example, on just about every page, ChangingThePresent.org (see sidebar) has some user data that changes based on the current logged-in user. Figure 1 shows one section of our latest home page. (We're still trying to get it right, so it's likely to change.) This page presents a relatively simple problem. If you can determine whether the user has logged in, you can customize the view on the fly with Flash, JavaScript, DHTML, or other browser-based code. You can see that a logged-in user can log out or view his or her profile, and a logged out user can sign up or log in.
Figure 1. Logged-in and logged-out views on ChangingThePresent.org

Figure 2 shows a slightly more advanced view of the user's data, one that we use throughout the site. The two views in Figure 2 are dramatically different. In order to handle page caching, I must deal with all of the differences. For each logged-in user, I must replace the logged-out piece of the page with a piece that displays the logged in user's login ID and picture. Caching this bit of content presents another level of challenge because each user has different data.
Figure 2. Two distinct views

This behavior is not unique to ChangingThePresent.org. The minute you start to personalize the user's experience, you limit your use of unmodified Rails page caching, but with a little customization, you actually can cache these pages quite easily.
You can solve these problems in any number of ways. These techniques are the most compelling to me:
- You can live within the constraints of the Rails framework, striking page caching to use fragment caching instead.
- You can load most of the page, and then use JavaScript and Ajax to load the small dynamic portion of the page. Server-side code will detect whether the user is logged in, and then render the appropriate partial with Ajax.
- You can store some user state, such as whether the user is logged in, within a client-side cookie. Then, you can dynamically alter the appearance of the page with JavaScript based on the contents of the cookie.
Of the three techniques, I strongly prefer the third because the first and second techniques force the Rails application into the picture. If I want the ultimate in scalability, I want to deal with as much static content as possible. In this article, I'll focus on the third approach. Don't use this technique to store anything that's too sensitive to lose, like ICBM launch codes or credit card numbers. For our limited set of data, this solution works fine.
Show and tell, or hide and seek?
When I first experimented with caching the home page, I could have decided to simply replace the links with JavaScript. Think of this technique as a show-and-tell. Based on what you know about the logged in user, you can tell that user the right story by selectively replacing or injecting parts of your Web page with JavaScript. To break it down a little further, you would do the following:
- Create a Web page with only the common elements for all users.
- When the user logs in, place some data about the user, such as a login, into a cookie.
- You would then fill out the rest of the page by injecting HTML fragments with JavaScript based on the contents of the cookie.
For the ChangingThePresent home page, the show-and-tell technique was overkill because I had only two sets of links to show based on the logged-in user. I opted for a second technique that I'll call hide-and-seek. I show all of the page elements common to all users, and have a hidden version of every possibility of data for the changing parts of the display. This is the hide part. Then, based on the user's role, you can use JavaScript to find it in the document for display. This is the seek part. You might think that showing all possible versions of data is overkill, but it's actually quite common when you want to selectively enable various features for different security roles. And hide-and-seek is perfect for the ChangingThePresent home page. To implement the technique, do the following:
- Create a Web page with only the common elements for all users.
- Partition your users into types. Add a version of content for each of your user types. In my case, the types for the ChangingThePresent home page are logged in and logged out users. Initially, make this content invisible.
- When the user logs in, place some data that distinguishes a group of users, such as a user role or login status, into a cookie.
- When the user accesses the page, selectively show the version of the content for the user type.
For the ChangingThePresent home page, the hide-and-seek implementation is remarkably simple. Recall that the home page has the partial in Figure 1 that shows some links related to user accounts. The links change based on whether the user is logged in or not. The first job is to build all of the common content for the page. I'll not show that here. The second page is to show all of the dynamic content for all users, regardless of whether the user is logged in:
Listing 1. Creating all versions of dynamic content in a single view
<div id='logged_out'> <%= link_to "login", :controller => 'members', :action => 'login' %> <br /> <%= link_to "register", :controller => 'members', :action => 'signup' %> </div> <div id='logged_in' style="display: none;"> <%= link_to "your profile", :controller => 'profiles', :action => 'show' %> <%= link_to "logout" , :controller => "members", :action => "logout" %> </div> |
You might notice the my profile link.
Initially, that link pointed to a user-specific profile, but that would
break our home page caching. Instead, I just point the link to the index action without
any user ID.
The index action then redirects the user to the correct profile page:
Listing 2. Redirecting a user to the correct profile page
def index
redirect_to my_profile_url
end
|
In Listing 2, my_profile_url is a method that
determines the right profile URL based on the type of the user, which may
be a celebrity, advisor, or a member. Each has a separate profile page. At
this point, the application is fully functional, but you'd see four links total,
two links each for logged_in and logged_out:
- login
- register
- your profile
- logout
The next step is to capture a cookie that holds the current type of user. For ChangingThePresent, I create a cookie, at login time, that has the current login ID. Then, I destroy the cookie at logout time:
Listing 3. Creating and destroying cookies at login and logout
def login
if request.post?
self.current_user = User.authenticate(params['user_login'], params['user_password'])
...
if logged_in?
set_cookies
...
end
end
def logout
end
private
def set_cookies
cookies[:login] = current_user.login
cookies[:image] = find_thumb(current_user.member_image)
end
def logout
cookies.delete :login
cookies.delete :image
...
end
|
In Listing 3, logged_in? is a private method
returning true if the current user is logged in. The Rails methods above
create three cookies when you log in, and delete them when you log out.
Don't worry about the data. I don't need it yet. Just understand that I can
now tell if a user is logged in, without invoking the Rails framework. I do need to make sure that the expiration of the cookie
matches the expiration policy of the site. In my case it does, so I'm
ready for page caching.
The next step is to selectively hide and show the right entries based on
the user's cookie. I added the following JavaScript to public/javascripts/application.js:
Listing 4. JavaScript support to show and hide login divs
function readCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0;i < ca.length;i++) {
var c = ca[i];
while (c.charAt(0)==' ') c = c.substring(1,c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
}
return null;
}
function handle_cached_user() {
var login_cookie = readCookie('login');
var logged_in = document.getElementById('logged_in');
var logged_out = document.getElementById('logged_out');
if(login_cookie == null) {
logged_in.style.display = 'none';
logged_out.style.display = 'block';
} else {
logged_out.style.display = 'none';
logged_in.style.display = 'block';
}
}
|
The first function reads the value of a cookie from JavaScript and the second manipulates the DOM. You can simplify this code by using the Prototype library, but I'm including the basic DOM lookup to make things clear to all readers. The final step is to invoke the JavaScript function when the page loads. I add the following to my layout:
Listing 5. Invoking the JavaScript functions when a page loads
<script type="text/javascript">
window.onload = function() {
handle_cached_user();
<%= render_nifty_corners_javascript %>
<%= yield :javascript_window_onload %>
}
</script>
|
That's simple JavaScript. I load the handle_cached_user function when the page loads, which
in turn shows or hides the right bits. Now, I can safely enable page
caching by adding the following to my controller:
caches_page :index
|
And it all works perfectly. I still need to periodically delete my front page from the cache
if I ever want that page to expire. To do so, I simply periodically delete
public/index.html. The hide-and-seek approach
works well for pages that have several classifications of users, but it
will not work for the user partial that you see in Figure 2. For that one,
I will need to use a combination of hide-and-seek and show-and-tell
techniques.
Take another look at Figure 2. I will use hide-and-seek to select the right version of the partial — depending on whether the user is logged in — and then use the show-and-tell technique to populate the dynamic parts of the page based on the contents of the cookies I wrote earlier in Listing 3, lines four and five. Remember, for show-and-tell, I specifically change elements of a page to conform to one user.
First, here's the static content that renders each partial: one for a
logged out user, and one for a logged in user. I'll assume the user is
logged out, so I will hide the logged_in div by attaching a style of display: none. Later, I can show or hide them with
JavaScript, if necessary. Notice that I use the same two names, logged_in and logged_out,
to identify each div, so I won't have to modify the JavaScript I wrote for
the home page:
Listing 6. Rendering both the logged in and logged out partials
<div class="boxRight sideColumnColor">
<div id='logged_in'>
<%= render :partial => 'common/logged_in' style="display: none; %>
</div>
<div id='logged_out'>
<%= render :partial => 'common/logged_out' %>
</div>
</div>
|
Next, here's the content for the logged_in partial. Notice that each HTML component containing dynamic content has an ID so I can find it with JavaScript and replace it later:
Listing 7. Showing the
logged_in partial
<div id='logged_in' style="display: none;">
<%= link_to %(<span class="mainBodyDark">Hi, </span>) +
%(<span class="textLarge mainBodyDark"><b id='bold_link'>) + "my_login" +
%(</b></span>), {:controller => 'profiles', :action => 'show', :id => 'my_login'},
{:id => 'profile_link'} %>
<br/>
<div id='picture_and_link'>
<a href="http://member/my_login" id='link_for_member_thumbnail'>
<img id='member_thumbnail'
alt="Def_member_thumbnail"
src="/images/default/def_member_thumbnail.gif" /></a>
</div>
<div id="not_mine">Not my_login?</div>
<br/>
<%= image_button "logout", :controller => "members", :action => "logout" %>
|
If you know Rails well, you probably noticed a few custom helper functions.
You can see four distinct pieces of dynamic content that I will need to
replace for each page load with JavaScript: the login in 3 places, and in one place, the
member image. The JavaScript code consists of a modification in the
handle_cached_user function, and a new method to handle the page update for
the dynamic user. I've simplified the code a little bit for this article.
Add the following function to your application.js file:
Listing 8. Replacing elements of the user partial
function handle_user_partial() {
var login_cookie = readCookie('login');
var image_cookie = readCookie('image');
var profileLink = document.getElementById('profile_link');
profileLink.href = '/member/' + login_cookie;
document.getElementById('bold_link').firstChild.nodeValue=login_cookie;
document.getElementById('not_mine').firstChild.nodeValue="Not " + login_cookie + "?";
document.getElementById('link_for_member_thumbnail').href="/member/" + login_cookie;
document.getElementById('member_thumbnail').src=image_cookie.replace(/%2[Ff]/g,"/");
document.getElementById('member_thumbnail').alt=login_cookie;
}
|
In Listing 8, the JavaScript function first reads the cookies and gets one part of the DOM tree: the
link to the current user's profile, called the profile_link. Next, the handle_user_partial:
- substitutes the name of the logged in user (which is stored in login_cookie) for my_login to create the correct URL for the user's profile page.
- inserts the name of the logged in user into the DOM element containing the bold text signifying the logged in user.
- inserts a simple sentence, "Not login?", into the DOM element containing the
logoutcaption in theloginpartial. - finds the dom element containing the member image, and replaces the image URL for a generic image with the URL for the member's image, which is in the
image_cookie. - also replaces the
alttag for the image with theloginname, just in case the image does not appear.
When you navigate a DOM, you'll find that sometimes, you need
to go directly to a DOM element, and sometimes you need a specific child of
that element, such as when you're dealing with text. I used the firstChild function to find the first child element of
the DOM item I wanted to find. The Prototype library makes dealing with
specific DOM elements a little easier with friendlier syntax, but that's
beyond the scope of this article.
Since I've already created all of the cookies, the final step is to call
the JavaScript from our existing handle_cached_user function. Remember, that function
is in public/javascripts/application.js:
Listing 9. Adding the
handle_user_partial function to handle_cached_user
function handle_cached_user() {
var login_cookie = readCookie('login');
var logged_in = document.getElementById('logged_in');
var logged_out = document.getElementById('logged_out');
if(login_cookie == null) {
logged_in.style.display = 'none';
logged_out.style.display = 'block';
} else {
handle_user_partial();
logged_out.style.display = 'none';
logged_in.style.display = 'block';
}
}
|
Notice the additional line in handle_cached_user within the else condition. That line will make the appropriate
substitutions before making the logged_in DOM
elements visible. All that remains is to use the page caching directives
you saw in this article and last month's to cache full pages.
This advanced technique has opened many doors for us. At ChangingThePresent.org, we estimate that we'll be able to cache more than 75% of our pages with very simple time-based sweepers. By using only slightly more sophisticated sweeping techniques, we will be able to cache well over 90% of our page hits, and perhaps more. When you factor in our aggressive image caching plans, you'll only hit the application server for 1% to 3% of all Web requests.
Keep in mind the down side. I've added significant complexity to this system. I must maintain more complicated HTML code, and make sure that my HTML and JavaScript stay in sync. But the upside is that I can use the simplest and most effective caching technique when I do need to get better performance. You can give it a try — go to ChangingThePresent.org and load the home page. Next, load each of the top level menus. You will find that we page cache four of the six top level menu choices. Create an account and reload each. Can you guess which pages are cached? In the next article, I'll dive into techniques that will improve your ActiveRecord performance as you continue to dive into Real world Rails.
Learn
-
See
all the articles in the Real world Rails series
-
See
JavaScript Prototype library: A library that makes the HTML DOM tree easier to navigate.
- Java To Ruby: Things Your Manager Should Know (Pragmatic Bookshelf, 2006): The author's book about when and where it makes sense to make a switch from Java programming to Ruby on Rails, and how to make it.
- Changing The Present: The nonprofit marketplace where you can give a donation gift consisting of an acre of a rain forrest, sight for a blind man, or an hour of a cancer researcher's time. This site serves as a foundation for this article series.
-
"Ruby
on Rails Advanced Page Caching" : This article walks you through some advanced page
caching techniques, and the JavaScript providing the foundation for the code to read the Rails cookie. The article explicitly covers Roles-based security and its relationship to page caching.
Get products and technologies
- Ruby on Rails: Download the open
source Ruby on Rails Web framework.

Bruce Tate is a father, mountain biker, and kayaker in Austin, Texas. The CTO of WellGood, LLC and the chief architect behind ChangingThePresent.org, he's also the author of nine books, including Beyond Java, From Java to Ruby, and Ruby on Rails: Up and Running. He spent 13 years at IBM and later formed the RapidRed consultancy, where he specialized in lightweight development strategies and architectures based on Ruby, and in the Ruby on Rails framework. He now works with a team of Rails developers to build and maintain the charity portal, ChangingThePresent.org.
Comments (Undergoing maintenance)





