The Google Analytics asynchronous tracking code came out of beta two weeks ago, and given my prior post on how scripts block page loads, I figured I’d briefly point out some interesting aspects of how the new tracking code works. The two things that I find interesting are (1) the asynchronous script loading and (2) the new syntax related to queuing events.


Which browsers support the async script attribute?

ga.async = true;
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXX-X']);
_gaq.push(['_trackPageview']);
(function() {
    var ga = document.createElement('script'); ga.type = 'text/javascript';
    ga.async = true;
    ga.src = ('https:'   == document.location.protocol ? 'https://ssl'   : 'http://www') + '.google-analytics.com/ga.js';
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();

It appears that setting the “async” attribute to true makes the magic happen, but if you look into which browsers currently support the async attribute, you’ll discover that it’s only Firefox 3.6+. So what gives?

It turns out that if you insert a script—that you created using document.createElement—dynamically into a page, then it will be asynchronous in most browsers. I ran some tests when this code was first released, and my results are included below.

Note: this table describes for each browser whether the the domContentLoaded event waits for an “async script” to finish before triggering, or if it goes ahead and triggers while the script is still loading. It does the same for window.load and also shows if a script that appears lower on the page will wait for an “async script” to load before executing or if it just goes ahead.

domContentLoaded event (jQuery) window.load event Execution of lower script on page
Mac:
Safari 4.0.4 goes waits goes
FF 3.5.7 waits waits waits
FF 3.6 goes waits goes
Opera 10.10 waits waits waits
Chrome goes waits goes
 
Windows:
IE6 goes goes goes
IE7 goes goes goes
IE8 goes goes goes
FF2 goes waits waits
FF3.6 goes waits goes
Chrome goes waits goes

As you can see, the most important actions—the domContentLoaded event firing and a lower script executing—can both happen before the “async script” finishes loading for all of these browsers except for Opera and Firefox less than 3.6. It’s mostly asynchronous… mostly.


The asynchronous syntax: _gaq.push()

There’s a new syntax for tracking events, and also for setting the initial variables (like your account id) which looks like this:

var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXX-X']);
_gaq.push(['_trackPageview']);

and:

<a href="#" onClick="_gaq.push(['_trackEvent', 'Videos', 'Play', 'How is babby formed?']);">Play</a>

The magic happens when the script actually loads and then replaces the _gaq array with a new object that immediately executes all previously queued events and has a “push” method that will also immediately execute any event whenever it is called. The code for the new _gaq object looks something like this (a little simplified):

var GoogleAnalyticsQueue = function () {

    this.push = function () {
        for (var i = 0; i < arguments.length; i++) try {
            if (typeof arguments[i] === "function") arguments[i]();
            else {
                // get tracker function from arguments[i][0]
                // get tracker function arguments from arguments[i].slice(1)
                // call it!  trackers[arguments[i][0]].apply(trackers, arguments[i].slice(1));
            }
        } catch (e) {}
    }

    // more code here…
};

// get the existing _gaq array
var _old_gaq = window._gaq;

// create a new _gaq object
window._gaq = new GoogleAnalyticsQueue();

// execute all of the queued up events - apply() turns the array entries into individual arguments
window._gaq.push.apply(window._gaq, _old_gaq);

It’s a pretty cool idea that should make Computer Science professors who love interfaces really happy. If you want to read some more about the asynchronous tracking code, Mathias Bynens wrote a great post describing how to optimize the asynchronous Google Analytics snippet—it has some real passion for minimizing bits.

Extra: the “apply” method

thisarguments
this
my_func.apply(this, ['foo', 'bar']);
my_func.call(this, 'foo', 'bar');