 | Level: Intermediate Bruce Tate (bruce@rapidred.com), CTO, WellGood LLC
26 Jun 2007 Normally, user-related content defeats page caching because the content for each user is subtly different. Using JavaScript
with cookies, you can use page caching even when you're displaying some
custom user data. This article explores advanced page caching in Ruby on
Rails.
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
 | |
In the Real world Rails series, international author and speaker Bruce Tate takes you through real-world Rails development, from the inside. As the CTO for WellGood, LLC, he is responsible for designing, building, and maintaining ChangingThePresent.org, a charity donations portal where you can donate an hour of a cancer researcher's time, preserve an acre of the rain forest, or sponsor the cataract surgery to make a blind person see. Hundreds of thousands of users have found thousands of nonprofits on ChangingThePresent to date, and the site continues to grow in popularity and scale.
You can find dozens of articles that will help you build simple Rails applications. This series will take you beyond the basics of building a simple blog, and into the issues that every Rails site must solve. You'll learn how to optimize Rails, and how to make your site more stable. You'll also learn how to work around base limitations of Rails through adding plug-ins. After reading each article in the series, you'll know a little more about how to make Rails sites work in the real world.
|
|
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.
Implementing hide and seek
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:
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.
Implementing show-and-tell
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
logout caption in the login partial.
- 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
alt tag for the image with the login name, 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.
Wrapping up
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.
Resources 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.
About the author  | 
|  | 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. |
Rate this page
|  |