Skip to main content

Real world Rails, Part 2: Advanced page caching

Using JavaScript and cookies to expand page caching

Bruce Tate (bruce@rapidred.com), CTO, WellGood LLC
Bruce Tate
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.

Summary:  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.

View more content in this series

Date:  26 Jun 2007
Level:  Intermediate
Activity:  3357 views

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:

    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.

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

Get products and technologies

  • Ruby on Rails: Download the open source Ruby on Rails Web framework.

About the author

Bruce Tate

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)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development
ArticleID=236576
ArticleTitle=Real world Rails, Part 2: Advanced page caching
publish-date=06262007
author1-email=bruce@rapidred.com
author1-email-cc=bruce.tate@j2life.com

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Rate a product. Write a review.

Special offers