Saturday, June 7, 2008

AJAX Google API to Minify Javascript using Ruby on Rails

posted by Jonah Dempcy

How to optimizing JavaScript performance in Ruby on Rails. This will only focus on one aspect of JS performance optimization, namely, writing a build script to concatenate/minify the JS, and setting up Rails to easily toggle between the compressed and normal files. Also, if your site uses a JavaScript library, we'll explore including it from Google's AJAX Libraries API.

The reason to do these optimizations are for client-side performance. Concatenating many files together into one is especially effective: 10 1-kilobyte files are much slower to download than a single 10k file. You might not think it makes much of a difference, but I estimate that removing 10 additional JS files, for instance, will shave 500-1000ms off latency. Plus, all of the time spent loading the JS will leave the page blank, if you put it in the head.

We'll need to be able to easily toggle between fully commented code and minified code, both for code hosted by us and code from the Google AJAX Libraries API. Since Google offers both minified and normal versions of the code, this should be no problem.

Including JavaScript in Ruby on Rails

First of all, let's look at how JavaScript is included in Ruby on Rails, the javascript_include_tag method. The method quite simply takes any number of source URLs and returns an HTML script tag for each of the sources provided. For instance:

  javascript_include_tag("script1.js", "script2.js", "script3.js")

Result:

<script type="text/javascript" src="/javascripts/script1.js"></script> <script type="text/javascript" src="/javascripts/script2.js"></script> <script type="text/javascript" src="/javascripts/script3.js"></script>

There's a little bit of magic you can do which is to include all JavaScript files in the /public/javascripts folder automatically, as well as having them concatenated into a single file. It looks like this:

  javascript_include_tag :all, :cache => true # when ActionController::Base.perform_caching is true

The resulting code is:

<script type="text/javascript" src="/javascripts/all.js"></script>

If you don't want them concatenated into a single file, e.g. for development purposes, you could do something like this:

# config/environments.rb:
DEBUG_JS = false;

# environments/development.rb:
DEBUG_JS = true;

# app/views/layouts/site.html.erb or wherever you include the JavaScript in your site:
<%=   javascript_include_tag(:all, :cache => true) if DEBUG_JS == false || ENV["RAILS_ENV"] != "development"
 javascript_include_tag(:all) if DEBUG_JS == true && ENV["RAILS_ENV"] == "development"
%>

I'm not sure how to integrate JSMin, YUICompressor or any other minification into the cached file, though, so I will explore the options for manual building and inclusion next.

Something else to remember is that caching needs ActionController::Base.perform_caching to be true in order to work. This is the default in production, but not development, though of course you can override it in your environments/development.rb file. For more information, view the docs for the javascript_include_tag.

JavaScript Concatenation and Minification

Next, we'll write a custom build script to generate a concatenated, minified version of our files. Ideally, Rails would automatically run it through a minifier, but I'm not sure how to set that up. In the meantime, this is a working solution that is pretty straightforward and requires minimal effort. I wish I were more skilled at shell scripting (and I highly urge readers to contribute their own code), but I'll show my implementation as an example regardless.

I'm on a Windows machine and wrote it as a batch file (.bat), but you can easily do this on linux, just using pipe (the | character) instead of the two right-angle brackets (>>). Here goes:

java -jar yuicompressor-2.3.5.jar "script1.js" -o main.js
java -jar yuicompressor-2.3.5.jar "script2.js" >> main.js
java -jar yuicompressor-2.3.5.jar "script3.js" >> main.js
java -jar yuicompressor-2.3.5.jar "script4.js" >> main.js

Nothing too sophisticated, just a straightforward script that generates main.js using the YUICompressor. To use it, simply run the batch file in the same folder as the JavaScripts and yuicompressor-2.3.5.jar. It will output a main.js file. I chose to run it separately for each individual file so if there are any errors, you can see where they occurred. If you concatenate all of the files into one and then compress it, debugging is difficult as the line number which threw the error is unknown.

I chose to use YUICompressor over JSMin since it seems to have better error messaging, warnings, and be a bit more strict. One of my former colleagues did some tests that determined that JSMin was faster in terms of using less CPU than YUICompressor (and certainly for minifiers which use eval(), such as Dean Edwards' packer). Unfortunately, I can't remember the specifics of the test or which platforms it was on, so that data is pretty much worthless. In any case, I decided on YUICompressor but you can use JSMin or whichever minifier you prefer.

If you are going to use YUICompressor, you must have Java installed and included in your path. If it isn't in your path, you can use the full path to Java, for instance, "C:\Program Files\Java\bin\java.exe" -jar [filename] [options].

Using the Google AJAX Libraries

Depending on your opinion about Google's recent offer to host the world's JavaScript frameworks with its AJAX Libraries API, you may not find this suggestion too useful, but I think it's helpful, especially to those without access to their hosting provider's response header settings, to present here. Without going into too much detail, suffice it to say that besides the caching benefit of using Google's API, their servers are configured "correctly" for best performance (far future expires headers, Gzip, etc).

One thing, though, is that you will want to include a minified version of the JavaScript in production and a full version in development, to ease debugging. It's impossible to debug minified JavaScript (don't even try), so we have to set a toggle like in the above example. Say your site is including jQuery. You may want to include a minified version on the live server but read the full code locally.

Toggling Minified Google JS

Here's how you can include minified jQuery in the production Ruby on Rails code while including the full jQuery library, comments and all, in development:

<%=   
javascript_include_tag("http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js",
                                            :all, :cache => true) if DEBUG_JS == false || ENV["RAILS_ENV"] != "development"

 javascript_include_tag("http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.js",
                                          :all) if DEBUG_JS == true && ENV["RAILS_ENV"] == "development"
%>
<

Or, if you didn't want to include everything in the JS folder, and wanted to granularly include only specific files, you might rewrite it like so:

<%= if DEBUG_JS == false || ENV["RAILS_ENV"] != "development"
   javascript_include_tag ("http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js",
                                            "main.js)
    elsif DEBUG_JS == true && ENV["RAILS_ENV"] == "development"
    javascript_include_tag("http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.js",
                                            "script1.js",
                                            "script2.js",
                                            "script3.js",
                                            "script4.js")
 end %>

That's all there is to it! Just switch the DEBUG_JS flag when you want to test in development mode with/without compression, and don't worry about when it's in production since it will always serve up the minified, concatenated JavaScript. The logic ensures that you won't accidentally start serving up the separate files in production, while giving you the flexibility to store your JavaScript in as many files as you like without negative consequences on the front end, as well as reading fully-commented code./p>

Labels: , , ,

Saturday, May 17, 2008

The Group Class in MooTools

posted by Jonah Dempcy

The MooTools Group class is for grouping a collection of objects and calling a function when all of those objects have fired a particular event. For instance, you could group an array of Element objects and fire an event when all of them have been clicked. Or, you could group a collection of Ajax request objects and fire an event when all have completed. (Both of these examples are straight from the MooTools 1.2 Beta docs for Plugins/Group).

How is this useful? Well, it's a convenience for when events depend on each others' completion but occur asynchronously, either by Ajax or through use of timers. Let's look at an example of how to handle dependent events without grouping first, to see where there is room for improvement:

// Flags to keep track of the request status
var firstRequestIsComplete = false;
var secondRequestIsComplete = false;

window.addEvent('domready', function() {
   var firstRequest = new Request({
       url: 'example-1.html',
       onSuccess: function() {
           firstRequestIsComplete = true;           
           if (secondRequestIsComplete) {
               console.log('Both requests are complete.');
           }
       }       
   }).send(); // Fire it
      
   var secondRequest = new Request({
       url: 'example-1.html',
       onSuccess: function() {
           secondRequestIsComplete = true;           
           if (firstRequestIsComplete) {
               console.log('Both requests are complete.');
           }
       }       
   }).send(); // Fire it   
});

Obviously, this isn't a stellar solution. Having to keep track of which Ajax requests have already fired using flags is a hassle, and this example only uses two requests. Say you had 10 different requests and you wanted to fire an event once all of them had completed. You'd need to store flags for which request had fired and which hadn't, perhaps in a hash table, and write a function that checks if all of the requests had fired or not. Then, for the onSuccess event handler of each request, you'd need to set a condition that calls the function to check if all had succeeded, and performs your desired action if so (in the above example, logging to the console).

What a hassle! All of this checking and logic duplicated in each request; there must be a better way.

Grouping Ajax Requests in MooTools

Ajax Ajax by nickwheeleroz

As it turns out, there is a better way, thanks to MooTools. Let's see how this code is cleaned up by the use of grouping with MooTools' Group class:

window.addEvent('domready', function() {   
   var firstRequest = new Request({url: 'example-1.html'});
   var secondRequest = new Request({url: 'example-1.html'});           

   var ajaxRequests = new Group(firstRequest, secondRequest);
   ajaxRequests.addEvent('onSuccess', function() {
       console.log('Both requests are complete.');
   });
  
   // Fire requests
   firstRequest.send();
   secondRequest.send();
});

All right! Now we've removed the duplicate logging code and onSuccess events, which were previously declared in two places. This code has the exact same effect as the previous implementation, but we're following the "write once" principle.

You could make it even more concise using chaining, if that's your thing:

window.addEvent('domready', function() {   
   new Group(new Request({url: 'example-1.html'}),
                       new Request({url: 'example-1.html'}))
       .addEvent('onSuccess', function() {
           console.log('Both requests are complete.');
       }).instances.each(function(instance) {
           instance.send();
       });
});

To me, the chaining isn't necessary here and iterating over the instances array stored in the group object probably isn't the best idea, since it's exposing the inner workings of the class by direct reference, rather than through the API. So, this example is only shown for the purposes of demonstrating how you can chain functions with MooTools, if you so desire.

Caveats and Gotchas with MooTools Groups

There are a few crucial gotchas that will hang you up if you aren't aware of them. First, the API documentation for MooTools 1.2beta is not accurate. It may be accurate for 1.1, but it certainly isn't for 1.2. They show an example of grouping using the Ajax class which was renamed Request in MooTools 1.2. The docs call the request() method to initiate an Ajax request but in actuality, it is the send() method. Can you spot any other inaccuracies in the example provided by the docs?

// From http://docs12b.mootools.net/Plugins/Group
//
// NOTE: This code will not work for MooTools 1.2.
// Shown for educational purposes only.

var xhr1 = new Ajax('data.js', {evalScript: true});
var xhr2 = new Ajax('abstraction.js', {evalScript: true});
var xhr3 = new Ajax('template.js', {evalScript: true});

var group = new Group(xhr1, xhr2, xhr3);
group.addEvent('complete', function(){
   alert('All Scripts loaded');
});

xhr1.request();
xhr2.request();
xhr3.request();

A few other problems with the example: It shows the onComplete event instead of onSuccess, and the arguments passed to the Ajax constructor are incorrect as well. Finally, it shows the event to monitor without the 'on-' prefix. Given the plethora of inaccuracies in the docs, I believe these nuances warrant further discussion. Let's look at each of these in turn.

The Ajax class was renamed to Request in MooTools 1.2 and the API was significantly overhauled. Instead of calling request() to initiate the request, now you must send() it. Now, the constructor takes an options object which has properties for all of the arguments, such as url. In the example given, the URL is passed as the first argument, but in the current code you provide it as a property of the options object instead:

// This code demonstrates the differences in how to make
// Ajax requests between MooTools 1.1 and MooTools 1.2:

// Old way
new Ajax('path/to/your/data', {
   // options go here
}).request(); // fire the request

// New way
new Request({
   url: 'path/to/your/data'
   // options go here
}).send(); // fire the request

As you can see, the URL is passed in via a property of the options object, rather than as the first argument of the constructor. I prefer this, as the MooTools API has been made much more consistent in 1.2 with most classes simply taking an options object, rather than a variety of parameters.

Another problem with the example provided in the docs is that it shows the onComplete event when this has been changed to onSuccess. And, to make matters worse, the docs exclude the 'on-' prefix in their example. Compare the right and wrong ways to monitor group events in MooTools 1.2:

// Wrong way (as shown in docs)
group.addEvent('complete', function(){
   alert('All Scripts loaded');
});

// Right way
group.addEvent('onSuccess', function(){
   alert('All Scripts loaded');
});

This is kind of odd, I'll admit, and inconsistent with the addEvent() API. Usually, you register events without the 'on-' prefix. But, for some reason, when you monitor an event in the group, you must use the 'on-' prefix. I discovered this after 20 minutes fruitlessly searching the web and stepping through my code, so hopefully this gem of information will save others that same trouble.

Testing MooTools Ajax Locally

Something crucial to be aware of when testing these examples is that you must serve the response to the Ajax request from a web server. If you access the page through the file:// protocol instead of http:// protocol, certain headers will not be present and MooTools won't fire the onSuccess event.

There's actually a funny story about this: At my old job, there was a contest to build an Ajax widget using various JavaScript frameworks. The MooTools team got hung up because they were testing through the file:// protocol (without a web server, just by opening the file directly with a web browser) and the Ajax requests were never firing their onComplete event handlers (this was back in MooTools 1.1 where it was called onComplete instead of onSuccess). Well, what's funny about it is that the Ajax request would fire onFailure, but it called the event handler with the Ajax response! The team building the widget actually got it working fine but it was "failing" every time-- the Ajax callback function was registered to the onFailure event.

So, the moral of the story is, try to avoid hacky workaround like this and serve up the Ajax responses the right way, through a web server.

Besides Ajax, what are some other uses of groups you can think of? Post your ideas in the comments and they may become the basis of future articles.

Labels: , ,