04 August 2014

Impetus

It all started with this post.

I noticed that the page would load and display the header and then stall while it loaded the Google Charting API. “I’m sure this could be improved quite easily!” I said naively. “There’s no way this problem is super complicated because JavaScript is terrible!”

Nngh.

Deferred Loading of JavaScript

There are a few ways to get JavaScript to download in the background while your page continues to be rendered by the browser. I’ll cover each individually in order of descending Kummerspeck.

The async keyword

The async keyword in the script tag is a pretty decent enough approach if you don’t need your scripts loaded in any particular order and don’t have to do any special logic after the page loads.

Here’s a basic example:

<script type="text/javascript" src="whatever.js" async></script>

This will fire-and-forget the loading of the JavaScript. Someone somewhere on the Internet recommended that the script end with andThenExecuteSomePageLoadLogic() instead of having a callback on your page, but sometimes you don’t have direct control of the source file you’re loading.

The DOM injection approach

Google Analytics and others use this approach to inject a script tag into the DOM and load the JavaScript asynchronously. The injection allows you to do more prep-work than just using the async keyword. Check out Google Analytic’s implementation:

1
2
3
4
5
6
7
8
9
10
11
<script type="text/javascript">
  var _gaq = _gaq || [];
  _gaq.push(['_setAccount', 'account-slug-here-lol']);
  _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);
  })();
</script>

You can see on line 7 the creation of the HTML element, and it gets added to the DOM on line 9. The startup logic on lines 2-4 are voodoo to me.

So, the thing I was trying to load (and then execute logic on completion) was Google’s Charting API, which it turns out writes directly to the DOM using (I think) document.write. This is slightly problematic, because if you use document.write after the page is done loading, the page will go away and be replaced by whatever you write, ending up with a blank page. Somewhat infuriating! But luckily someone figured this out and saved me from having a stroke.

JQuery to the rescue

I love JQuery except when I’m using it.

JQuery has a special function called getScript that is based on its ajax function. Its job is to load a file and then execute it, which seems unremarkable, until you see that you can chain a done and fail function off the end of it, like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script type="text/javascript">
  (function () {
    $.getScript("http://www.google.com/jsapi").done(function () {
      var callback = function () {
	    /* this is the thing i wanted to happen after jsapi was loaded */
        drawVisualization();
      };

      /* here, i have to include the callback property to avoid having
         the DOM rewritten. see the link to stackoverflow above. */
      google.load('visualization', '1', {packages: ['corechart'], callback: callback});
    });
  })();
</script>

You can see the done callback on line 3. The function defind on line 2 is executed after creation (line 13) and calls the getScript function, loading the Google Charting API. When that’s done, google.load is called. When that’s done, drawVisualization is called and the pretty chart shows up.

Nnngh

A word of caution: when googling around for solutions to this problem you may see references to window.onload or statechange event handlers. Do not bite. Solutions based on these events are not very cross-browser friendly, and I’m about 70% sure they directly cause cancer.

Who would have thought making JavaScript work sanely would be so difficult?