Just want to skip to the good stuff? Check out our new ERB Caching technique now!
I've always had a love/hate relationship with Rails caching. The guys on the Rails team have supplied us with several tools for the job and have taken most of the pain out of it. Between page caching, action caching and fragment caching, you usually can find a method that will suit your application.
Easy as it may be, however, these caching techniques do come at a cost. The more caching you use,the more control you lose over your site and your user experience. These tradeoffs have proventhe most significant when developing social networking sites, where high traffic demands efficient caching but the need for user personalization demands constant fresh content.
So which do you use? Here are the pros and cons of the existing Rails caching methods, plus a brand new technique we are using at Killswitch we call ERB Caching.
Page Caching (The Unattainable Ideal)
From the perspective of performance and speed, nothing can beat the rewards of page caching. When rendering a page, Rails saves the generated markup as HTML into the /public folder. From that point on, subsequent requests for the same URL will be served by Apache, leaving Rails completely out of the process. This takes tremendous strain off of Rails and allows a single server to handle significant traffic. Even if the cached pages' time to live (ttl) is only a few minutes, the performance savings are fantastic.
Using page caching has worked well for us on some of our large scale projects. For one such project, we used the Akamai service to handle requests for cached pages, taking even more load off of our server. With this setup, a single server with only a few mongrel instances has been able to easily serve thousands ofunique visitors daily on a high profile, media-intensive site.
As ideal as this is, using page caching becomes an impossibility if you want any kind of personalization on your site (and you probably do). Since Rails is left out of the equation and Apache is serving static HTML files, it's nearly impossible to include any personalized content on the site (except for using Javascript trickery, of course). This is why page caching is used infrequently in the industry as users demand personalized, community oriented experiences.
Action Caching (Close, But Not Quite)
For more flexibility (and fewer performance gains) Rails supplies action caching. This feature is similar to page caching in that the entire output page is still cached. This time, however, Rails is in charge of serving thefile. This means that you can use before/after filters and sessions to determine what to send to the user. For example, you can send one page if the user is a logged in member, another if they are not.
With this additional layer of control, this technique is more applicable to community-driven apps. However, simple things like putting the logged-in user's name at the top of the page is still difficult/impossible, since all logged in users would be served the same cached page.
Many of the deficiencies of this method were fixed by the action_caching plugin, which allows for conditional caching, control over cache storage options and more. The functionality of action caching has also been significantly extended with our creation of ERB Caching, described below, which uses action caching as its foundation. More on that coming up in a few paragraphs...
Fragment Caching (It Works, But Still...)
Fragment caching has become the standard caching mechanism of large, dynamic, user-driven sites. It gives you all the flexibility you need, though the performance savings aren't much compared to those of page and actioncaching. This method allows you to mix generated content with chunks of the view that are cached and then reassembled on every render.
Using fragment caching helps speed up page renders, reduces database queries and is definitely better than using no caching at all! Still, I couldn't help feeling like there must be a better way. Working on social networking sites that could almost use action caching became frustrating - I wanted the benefits of fully cached pages and was tired of working with views littered with <% cache ... %> fragment caching code.
With that goal in mind, the idea for ERB Caching was developed.
ERB Caching (Finally, the Best of Both Worlds!)
I always had a sense that action caching was more powerful than it let on. You get the control and power of Rails, but still are given the performance benefits of caching the entire page.
My specific challenge was building a social networking site where every page on the site would have extra user-centric modules on it if the user was logged in. The modules are fairly simple, just some links and buttons that would initialize user actions. Still, their creation was dependent on knowing whether the user was logged in and knowing who they were.
The solution was to actually store ERB code in the cached views. The cache file is loaded into Rails, after which the ERB Caching code does a quick parsing to render any remaining ERB. The final file is then sent to the user, fresh and personalized.
Here's an arbitrary example:
In main_controller.rb:
before_filter :setup_vars caches_action :index def index @message = "Hello from index!" end def setup_vars @message = "Hello from before_filter!" end
In index.rhtml:
<p>This time will get cached and be the same on every page view: <%= Time.now %><p> <p>This time, however, will update on every page view: <%%= Time.now %><p> <p>The message set in the controller is: <%%= @message %><p>
When this view is first rendered, any ERB tags with two percent signs will not be rendered, but simply replaced by single percent signs. This is what the contents of the cache file will then look like:
<p>This time will get cached and be the same on every page view: Fri Feb 08 14:43:54 -0600 2008<p> <p>This time, however, will update on every page view: <%= Time.now %><p> <p>The message set in the controller is: <%= @message %><p>
The trick is then to render this a second time in an after_filter. Any variables set in a before filter will be available in this second rendering.
This is what the first page render produces:
<p>This time will get cached and be the same on every page view: Fri Feb 08 14:43:54 -0600 2008<p> <p>This time, however, will update on every page view: Fri Feb 08 14:43:54 -0600 2008<p> <p>The message set in the controller is: Hello from index!<p>
... and the second page view produces:
<p>This time will get cached and be the same on every page view: Fri Feb 08 14:43:54 -0600 2008<p> <p>This time, however, will update on every page view: Fri Feb 08 14:47:17 -0600 2008<p> <p>The message set in the controller is: Hello from before_filter!<p>
As you can see, any "double" ERB tag that is in a view will be cached as a single ERB tag and then rendered freshly on every request. This becomes very useful when you want to cache whole pages, but also want to have the current user's name displayed on the page. Simply set @user in a before_filter and then use <%%= @user.name %> in your view.
The code required to make this work is surprisingly simple and is all included in the erb_cache.rb file. Just place the file in /lib and put require 'erb_cache' in your environment.rb file. You will also have to put ActionController::Base.perform_caching = true in environment.rb if you want to test this out in development.
Also, don't forget to get the ActionCache plugin, it provides some excellent features.
This is still under development, so if you have any trouble with it, please let us know!
Installing and configuring the Globalize plugin can be difficult, but it usually goes easily if you follow the instructions. There's nothing I can say about that that's not thoroughly covered elsewhere. Adapting your coding style to use it is almost advertised.
But once you have it up and running there are a lot of subtle things that can still give you headaches down the line. For many of our international sites, the workflow is this:
- The client goes back and forth on designs, and we make a prototype like usual.
- Before getting started, we installed the Globalize plugin. Once we have the site roughed out and the design is approved, Globalize has automatically generated a list of the unique phrases in our app. (You might want to clear the translations table and re-test every page so your list doesn't include obsolete or phased out phrases).
- Then we hand the list to our client, who sends it to the team of translators, who send it back to us, and we slurp it into the database with a simple script (just loop through the languages and Locale.set, then Locale.set_translation)
That's technically all you need to do to globalize your app. But, if you want your application to be as scalable as possible—or you'd like to keep your developers from pulling their hair out—here are some tips.
- If at all possible, never translate phrases over 256 characters. If you do, you'll have to change globalize_translations.tr_key and and globalize_translations.text to a "text" field instead of a "string" (or varchar). That will work just fine, but, if you can avoid having to do that, it will really pay off to be able to add an index to tr_key and language_id.
- Make sure your validation messages are each translated individually, not en-masse in your view, or your translators will have extra work. That is, you want to avoid adding this to your translations table: "Errors: Name cannot be blank. Email address is not valid.", and instead have several seperate rows like this: "Errors:", "Name cannot be blank", "Email address is not valid" and so forth.
- You may have to turn off view caching in your environments/production.rb if you're running a mongrel cluster, or you might get spontaneous, random language changes as your users navigate the site! (change config.action_controller.perform_caching and config.cache_classes to "false")
- Don't stress too much about translating the word "Browse..." or "Choose File" on your file upload buttons- you can't change that! That's controlled by the user's browser, and it probably looks fine to them already.
- Lastly, here's a handy query for generating statistics on how many translations you've added to the system for each language:
SELECT english_name, COUNT(*) num_phrases FROM globalize_languages gl LEFT JOIN globalize_translations gt ON gl.id = gt.language_id GROUP BY english_name ORDER BY english_name
- And this rake task will generate a spreadsheet-checklist of phrases that still require translation.
translation_checklist.rake
Copy it to your lib/tasks folder, and run it to, in this case, make a pipe-delimited checklist, without a column for "English".
rake skip_languages=English delimiter="|" db:translation_checklist > whatever_file.csv
So you have this great community-based website. People are making profiles, uploading images and videos, voting on who's got the hottest dress and 'snagging' each other's cakes. What you need now is an interactive map that not only shows you where all of the parties are and how many people are going to be there, but also links you to the host's profile page so you can check out the car daddy is getting them for their birthday. OMG, you need XML!
Okay, so no one is likely to be squealing over the code behind the party map that Killswitch recently built for one of our entertainment clients—this kind of dynamic user data-driven application isn't even all that new. But I have to admit that I got a little excited over this and other similar projects that we've been doing at Killswitch thanks to the combination of ActionScript 3 and Ruby on Rails and how they handle XML.
These projects usually follow the pattern of users upload some sort of content (party info, images etc. ), data about that stuff gets stored in a database (city, name, longitude, latitude, file paths), then it's packaged up as XML and passed on to the flash movie (map) where that information is dressed up in its party outfit.
The benefit of using Rails on the back end is that it makes accessing the database and generating xml an easy, elegant and painless process. It can be as simple as adding one line of code:
render :xml => @parties.to_xml
Which parties this refers to is of course determined by the find conditions you set when the @parties object is created. If more control is needed—maybe there are fields in the database we don't want to make public or there is some sort of reformatting we want to impose on the data—we can render a template instead. This is done similarly to an html template.
ActionScript 3 is beneficial for the same reason that working with XML in Rails is convenient. It is nearly automatic. In previous versions of ActionScript if I wanted to get the name of the second guest attending a party as represented by this excerpt of XML:
<party>
<host>Seth</host>
<location>Chicago</location>
<guests>
<guest>Jasper Johns</guest>
<guest>Julie Mehretu</guest>
<guest>Robet Rauschenberg</guest>
</guests>
<time>9pm sharp</time>
</party>
I would have to write something like this:
//assuming the xml is loaded into partyXML var second_guests = partyXML.firstChild.childNodes[2].childNodes[1].firstChild;
This is not exactly very legible and becomes less so as we go deeper down the xml tree. What's worse is what happens when someone decides to change the structure of the tree. If time were listed before guests, we'd be looking for a list of names somewhere inside "9pm" and that wouldn't be conducive to anything good. The assignment to second_guest would have to be rewritten.
One alternative would be to write a function that parses the xml and formats it into an object that can be easily referenced later, but I'd rather spend my time animating something. After all, that is what flash is good for. This is where ActionScript 3 wins some praise. These days all it takes to find the second guest coming to my party is:
var second_guest = partyXML.guests.children()[1] // returns "Julie Meretu"
Finding the time would be:
var start_time = partyXML.time // returns "9pm sharp"
It is intuitive, legible and clean. I don't need to know the order in which things are listed in the xml. I only need to know how they are nested and what the tag names are. Not only does this save the headaches of maintaining cryptic code when the xml structure is changed, but it also saves a lot of time—time better spent on perfecting shape tweens or tricking out the user interface.
So there it is, your dynamic XML birthday cake fresh from the Rails bakery, ornately decorated with a layer of Flash-icing. Everybody nosh.

