Load everything asynchronously

A couple weeks ago there was rather a lot of excitement over the fact that Google released a new Analytics snippet that loads the tracking library asynchronously. This is indeed great news, for reasons pored over in the aforelinked articles. But let’s take a closer look at Google’s implementation:

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

  (function() {
    var ga = document.createElement('script');
    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 
        'http://www') + '.google-analytics.com/ga.js';
    ga.setAttribute('async', 'true');
    document.documentElement.firstChild.appendChild(ga);
  })();

For simple use cases this is fine, barring some quibbles over Google’s obfuscatory naming scheme for Analytics’ global public API objects and their bizarre use of underscored methods throughout. But say you have a page where you’re doing a lot of custom event tracking. The various event handlers in your page should be completely independent of each other, but this snippet introduces some complications. Each handler will need to figure out whether Analytics is loaded: do I call something synchronously using _gat and pageTracker, or do I push items onto _gaq? Analytics makes this slightly friendlier by replacing _gaq on load with an object whose push() method executes the given command immediately, but we’re still left with the problem of global state. Since we want all our event handlers to be independent and async-safe, each one will have to check whether we have a global _gaq object already and make one if we don’t:

  $('a').on('click', function() {
    window._gaq = window._gaq || [];
    _gaq.push(['_trackPageView', '/path/to/virtual/url']);
  });

This however is very brittle since it assumes that if Analytics is not yet loaded then it will be at some point, at which juncture your tracking command will be executed. To fix this properly you’d need to have a function that could wrap code in a block that checked whether Analytics was loaded before running the code, and that made sure Analytics was only loaded once. Something like this:

  writeToAnalytics = (function() {
    var loaded = false;
    
    var ensureLoaded = function() {
      if (loaded) return;
      loaded = true;
      var ga = document.createElement('script');
      ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 
          'http://www') + '.google-analytics.com/ga.js';
      ga.setAttribute('async', 'true');
      document.documentElement.firstChild.appendChild(ga);
    };
    
    return function(command) {
      window._gaq = window._gaq || [];
      _gaq.push(command);
      ensureLoaded();
    };
  })();
  
  $('a').on('click', function() {
    writeToAnalytics(['_trackPageView', '/path/to/virtual/url']);
  });

This may not seem like a terrible burden but it illustrates a larger point: having many independent events rely on global state or execution order does not scale to large codebases as it becomes increasingly hard to manage dependencies between functions that ought to be completely decoupled.

Thankfully, since its 2.1 release back in June JS.Class has allowed us to load just about anything we like asynchronously, including Analytics (Helium even ships with functions for this). Let’s have a look at the old blocking Analytics loader and figure out how to make it async:

  <script type="text/javascript">
  var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
  document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
  </script>
  <script type="text/javascript">
  try {
  var pageTracker = _gat._getTracker("UA-XXXXXX-X");
  pageTracker._trackPageview();
  } catch(err) {}</script>

We can use JS.Packages to write a loader function for this that will let us load Analytics on demand without using any global state checks. First, notice that the first script picks a domain to load Analytics from, then writes a script tag with the appropriate URL. The loaded script contains the Analytics _gat object. We can encapsulate this as:

  JS.Packages(function() { with(this) {
      
      var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
      file(gaJsHost + 'google-analytics.com/ga.js')
          .provides('_gat');
  }});

We can then add a custom loader function that uses our Analytics ID to make a pageTracker object, as done by Google’s second script. Our completed loader code looks like this:

  JS.Packages(function() { with(this) {
      
      var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
      file(gaJsHost + 'google-analytics.com/ga.js')
          .provides('_gat');
      
      loader(function(cb) {
          try {
              window.pageTracker = _gat._getTracker("UA-XXXXXX-X");
          } catch(err) {}
          cb();
      })  .provides('pageTracker')
          .requires('_gat');
  }});

We need to call cb() when our custom loader is done loading its objects to tell JS.Packages the loader has completed. Also note how we assign to window.pageTracker to make the object global, and we don’t call _trackPageview() here since our only concern is loading the object.

Now that JS.Packages knows how to load pageTracker we can just require() it wherever we need it, and JS.Packages will make sure it is only loaded if needed, and that it is only loaded once. Our event handler then becomes:

  $('a').on('click', function() {
    require('pageTracker', function() {
      pageTracker._trackPageView('/path/to/virtual/url');
    });
  });

You could even tidy this up using some higher-order functional programming:

  requiring = function() {
    var args = arguments;
    return function() { require.apply(this, args) };
  };
  
  $('a').on('click', requiring('pageTracker', function() {
    pageTracker._trackPageView('/path/to/virtual/url');
  }));

We’ve eliminated all knowledge about how the Analytics library gets loaded from our event handler and put the loading logic in one place: the JS.Packages configuration. Hopefully you can see that this scales to more complex codebases better than Google’s default snippets, and you can see how to write async loaders for any library you choose.

One final remark; notice how we were able to use JS.Packages to transform the legacy Analytics loader into an asynchronous loader without modifying our use of the Analytics API (aside from wrapping it in require() blocks). This strategy removes the need for the _gaq command queue since JS.Packages makes sure an object is loaded before letting you use it. I’m seeing far too many JavaScript libraries being tweaked to accommodate async loading, requiring developers to use a queue or hook into some custom event to run their code. We can have async loading without modifying our libraries, and we can do it in a way that is almost transparent to application code such that it scales very nicely with the size of your codebase. This is what JS.Packages provides, and I’d much rather we focus on a general-purpose package loader than have every framework out there solve the problem in its own idiosynchratic way.