With the page cache, Rails can no longer intervene. In a way, that's a good thing, because you can really get good performance. Rails simply creates an HTML page, puts it in a directory, and after that, it can be placed behind your head. From then on, the application server manages the pages and the pages go into the application server without any loops. From a performance perspective, page caching is a godsend.
I also love the page cache, and Rails makes it simple and neat. Caching can be enabled with just one line of code. If you add some more code, you can terminate the cache by simply deleting the file operation or by using the Rails higher layer API. There is a problem here. Not every Web site can use page caching. If the data on the page changes according to the user who accesses it, then the page cache cannot be made. And if it's hard to tell when a page is due to expire, the page caching requirements are too demanding.
For example, on almost every page, changingthepresent.org (see sidebar) has some user data that varies based on the current logged-on user. Figure 1 shows a portion of our latest homepage. (We've been trying to perfect it, so it's likely to change.) This page presents a relatively simple problem. If you can determine whether a user is logged in, you can dynamically customize the view with Flash, JavaScript, DHTML, or any other browser-based code. You may find that a logged-on user can log out of the system or view its profile, and the logged out user can sign up or sign in again.
Figure 1. Login and logout view on changingthepresent.org
Figure 2 shows a slightly more advanced view of the user data that our site uses. The two views in Figure 2 are very different. In order to process the page cache, I have to resolve all the differences first. For each logged on user, I have to replace the page logout to show the login ID and user picture of the logged-on user. Caching these content poses another level of challenge, because each user's data is different.
Figure 2. Two distinct views
This situation is not unique to the changingthepresent.org. If you need a personalized user experience, the use of the unmodified Rails page cache is limited. But if you don't have a lot of customization, you can actually easily cache these pages.
There are many ways to solve these problems. I prefer to use the following tips:
- Within the constraints of the Rails framework, cancel the page cache and replace it with a segment cache.
- Load most of the page first, and then use JavaScript and Ajax to load the smaller dynamic portions of the page. The server-side code can detect if a user is logged in, and then present the appropriate part with Ajax.
- Store certain user statuses, such as whether the user is logged in, in the client's cookie. Then, depending on the contents of the cookie, use JavaScript to dynamically change the appearance of the page.
Of these three techniques, I prefer the third because the first and second techniques involve the Rails application. For maximum scalability, use static content as much as possible. In this article, I will focus on the third approach. Do not use this method to store any sensitive data that cannot be lost, such as ICBM boot code or credit card number. This method works well for the limited data we are dealing with.
Use show and tell or hide and seek?
When I first started trying to cache the homepage, I could have simply replaced the links with JavaScript. This technique can be viewed as a show-and-tell. Based on our understanding of logged-in users, you can use JavaScript to selectively replace or inject portions of a Web page to provide users with the right experience. To further subdivide, I will do the following:
- Create a Web page that has only common elements for all users.
- When a user logs on, some data about the user is stored in a cookie, such as login information.
- Then, use JavaScript to populate the remainder of the page by injecting the HTML segment with the contents of the cookie.
For the Changingthepresent homepage, the show-and-tell technique is a bit too powerful because I only have two sets of links to display based on the users you log on to. So I chose the second technique, which I call hide-and-seek. First, the public page elements of all the users are displayed, and the part of the page is displayed with the possible hidden versions of each type of data. This is the hide part. Then, use JavaScript to locate the user's content in the file based on the user's role and display it. This is the seek part. You might think that showing all the possible versions of the data is a bit too powerful, and in fact, this is a very common way to selectively enable multiple features for different security roles. The hide-and-seek approach is ideal for the Changingthepresent home page. To implement this method, you can do the following:
- Create a Web page that has only common elements for all users.
- Partition the user by type. Add content versions for each user type. As far as I am concerned, the user type of the Changingthepresent home page includes the logged-in user and the logout user. Initially, make this content visible.
- When a user logs on, some data that can be divided into groups of users is stored in a cookie, such as a user role or login status.
- When a user accesses this page, the content version of the user type is selectively displayed.
Implementing Hide and Seek
For the Changingthepresent homepage, hide-and-seek is incredibly simple to implement. In the previous Figure 1, this home page has a section that shows some of the links associated with user accounts. These links can vary depending on whether the user is logged on or not. The first step is to build all the public content for this page. I do not give a specific approach in this article. The second page needs to display all the dynamic content of all users, regardless of whether the user is logged on:
Listing 1. Create 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>< C5/><div id= ' logged_in ' style= "display:none;" >
<%= link_to "Your profile": Controller => ' profiles ',: Action => ' show '%> <%= ' link_to
T ",: Controller =>" members ": Action =>" logout "%>
</div>
You may have noticed the My Profile link. Initially, the link points to a user-specific profile, but this may hinder our home page cache. Instead, I simply point this link to an index operation with no user ID. The indexing operation then redirects the user to the correct profile page:
Listing 2. Redirecting users 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 correct configuration file URL based on the user's type, which may be a celebrity, advisor, or member. Each user type has a separate profile page. At this point, the function of the program has been completed, you can see a total of four links, logged_in and logged_out each have two links:
- Login
- Register
- Your profile
- Logout
Next, get the cookie that contains the current user type. For Changingthepresent, I created a cookie at logon that contains the current login ID. Later, destroy the cookie when it is logged out:
Listing 3. Create and destroy cookies on 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
-
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 that returns true if the current user is logged on. The Rails method above creates three cookies at the time you log in and deletes them when logging out. There is no need to bother with data. No data is required. You can understand that without invoking the Rails framework, I can tell if a user is logged in. I do not need to make sure that the expiration of the cookie is consistent with the site's expiration termination rule. In my case, the two are consistent, so I can start the page cache now.
Next, you selectively hide and display the correct entries based on the user's cookie. Add the following JavaScript code to the Public/javascripts/application.js:
Listing 4. JavaScript code that supports the show and hide login div
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 cookie value from Javascript and the second function handles this DOM. This code can be simplified by using the Prototype library, but I include a basic DOM lookup for the reader to understand. The final step is to call the JavaScript function when the page loads. I added the following code to the layout:
Listing 5. Call JavaScript function when page is loaded
<script type= "Text/javascript" >
window.onload = function () {
handle_cached_user ();
<%= render_nifty_corners_javascript%>
<%= yield:javascript_window_onload%>
}
</script >
The above JavaScript code is simple. When the page loads, the Handle_cached_user function is loaded, and it displays or hides the correct content accordingly. Now, I can do this by adding the following code to the controller to enable page caching:
The above code works best. I still need to remove the previous page from the cache periodically so that I can expire the page. To do this, I simply delete public/index.html on a regular basis. The hide-and-seek approach works well for pages with several types of users, but it's not as good as the user partial shown in Figure 2. For the latter, hide-and-seek and show-and-tell techniques need to be used synthetically.
Implement Show-and-tell
Look at Figure 2 again. I will use hide-and-seek-to select the correct version of partial based on whether the user is logged on, and then use the show-and-tell technique to populate the dynamic part of the page based on the contents of the cookies I wrote in row 4 and line 5 of listing 3. Remember, for Show-and-tell, I specifically changed the elements of the page to fit a single user.
First, complete the static content rendered on the two partial (that is, the logged-in user and the logged-on user). I assume the user has logged out, so I will hide logged_in div via the additional display:none style. Then, if necessary, I can use JavaScript to show or hide them. Notice that I use the same two names: Logged_in and logged_out to identify each div so that I don't have to modify the JavaScript I wrote for this home page:
Listing 6. Render login and log out these two partial
<div class= "Boxright sidecolumncolor" >
<div id= ' logged_in ' >
<%= render:p artial => ' common/ Logged_in ' style= ' display:none; %>
</div>
<div id= ' logged_out ' >
<%= render:p artial => ' common/logged_out '%>
</div>
</div>
Next, complete the content of logged_in partial. Note that each HTML component that contains dynamic content has an ID so that I can use JavaScript to find it and then replace it:
Listing 7. Show 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 ' > </a> </div> <div id=" Not_mine ">not my_login?</div> <br/> <%= Image_button" Logout ": Controller =>" members ": Action =>" log Out "%>
If you have enough knowledge of Rails, you may notice several of these custom helper functions. There's a lot of dynamic content out there that I need to use JavaScript to replace for each loaded page: Three logins, a member image. The JavaScript code here makes a modification to the Handle_cached_user function and also includes a way to process page updates for dynamic users. For the specifics of this article, I made a little simplification of this piece of code. You can add the following functions to the Application.js file:
Listing 8. Replace elements of 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, this JavaScript function first reads this cookie and gets part of the DOM tree: The link to the current user profile, called Profile_link. And then the Handle_user_partial function:
- Replace the name of the logged-on user (stored in Login_cookie) with My_login to create the correct URL for the user profile page.
- Inserts a login user name into the DOM element, which uses bold text to represent the logged-in user.
- Inserts a simple sentence "not login" into the DOM element that contains the logout caption in the login partial.
- Find the DOM element that contains the member image, replace the URL of the general image with the URL of the member image, and save the member image in the Image_cookie.
- Also, replace the ALT tag of this image with the login name in case the image does not appear.
When navigating in the DOM, you will find that you sometimes need to go directly to a DOM element, and sometimes you need to go to a specific child element of that element, such as when working with text. I used the FirstChild function to look for the first child element of the DOM element as needed. Because the syntax is more user-friendly, the Prototype library makes it easier to work with specific DOM elements, but this is beyond the scope of this article.
I've created all the cookies, and the last step is to invoke JavaScript from the Handle_cached_user function. Keep in mind that the function is in public/javascripts/application.js:
Listing 9. Add the Handle_user_partial function to the 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 ';
}
Note that there are two additional lines of code below the Handle_cached_user function in the else condition. These two lines of code can be appropriately substituted before the logged_in DOM element is visible. All that remains to be done is to cache the entire page using the page caching instructions described in this article and last month's article.
Concluding remarks
This advanced technique introduced in this article opens up a lot of doors for us. On changingthepresent.org, we estimate that using a very simple, time based scavenger can cache more than 75% pages. By using a slightly more sophisticated cleanup technique, we can cache more than 90% of pages, and possibly more. If you want to try to influence our image caching program, you can only touch Web requests from application servers 1% through 3%.
But at the same time, we should also see the downside. I have added significant complexity to this system. I have to maintain more complex HTML code, and make sure that HTML and JavaScript can stay synchronized. But the good side is that I can use the simplest and most efficient caching techniques when it comes to better performance. You can also try this trick-to access changingthepresent.org and load the home page. Next, load each top menu. You will find that we will page cache four of the top six menus. Create an account number and overload each menu. Can you guess which page is cached? In the next article, as you continue to delve into the real world of Rails, I'll take you through some of the techniques that can improve ActiveRecord performance.