I’ve looked at a bunch of third party JavaScript snippets as part of my P3PC series. As I analyzed each of these snippets, I looked to see if scripts were being loaded dynamically. After all, this is a key ingredient for making third party content fast. It turns out nobody does dynamic loading the same way. I’d like to walk through some of the variations I found. It’s a story that touches on some of the most elegant and awful code out there, and is a commentary on the complexities of dealing with the DOM.
In early 2008 I started gathering techniques for loading scripts without blocking. I called the most popular technique the Script DOM Element approach. It’s pretty straightforward:
var domscript = document.createElement('script');
domscript.src = 'main.js';
document.getElementsByTagName('head')[0].appendChild(domscript);
Souders, May 2008
I worked with the Google Analytics team on their async snippet. The first version that came out in December 2009 also used appendChild, but instead of trying to find the HEAD element, they used a different technique for finding the parent. It turns out that not all web pages have a HEAD tag, and not all browsers will create one when it’s missing.
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);
Google Analytics, Dec 2009
Google Analytics is used on an incredibly diverse set of web pages, so there was lots of feedback that identified issues with using documentElement.firstChild. In February 2010 they updated the snippet with this pattern:
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);
Google Analytics, Feb 2010
I think this is elegant. If we’re dynamically loading scripts, we’re doing that with JavaScript, so there must be at least one SCRIPT element in the page. The Google Analytics async snippet has just come out of beta, so this pattern must be pretty rock solid.
I wanted to see how other folks were loading dynamic scripts, so I took a look at YUI Loader. It has an insertBefore variable that is used for stylesheets, so for scripts it does appendChild to the HEAD element:
if (q.insertBefore) {
var s = _get(q.insertBefore, id);
if (s) {
s.parentNode.insertBefore(n, s);
}
} else {
h.appendChild(n);
}
YUI Loader 2.6.0, 2008
jQuery supports dynamic resource loading. Their code is very clean and elegant, and informative, too. In two pithy comments are pointers to bugs #2709 and #4378 which explain the issues with IE6 and appendChild.
head = document.getElementsByTagName ("head")[0] ||
document.documentElement;
// Use insertBefore instead of appendChild to circumvent an IE6 bug.
// This arises when a base node is used (#2709 and #4378).
head.insertBefore(script, head.firstChild);
jQuery
All of these implementations come from leading development teams, but what’s happening in other parts of the Web? Here’s a code snippet I came across while doing my P3PC Collective Media blog post:
var f=document.getElementsByTagName("script");
var b=f[f.length-1];
if(b==null){ return; }
var i=document.createElement("script");
i.language="javascript";
i.setAttribute("type","text/javascript");
var j="";
j+="document.write('');";
var g=document.createTextNode(j);
b.parentNode.insertBefore(i,b);
appendChild(i,j);
function appendChild(a,b){
if(null==a.canHaveChildren||a.canHaveChildren){
a.appendChild(document.createTextNode(b));
}
else{ a.text=b;}
}
Collective Media, Apr 2010
Collective Media starts out in a similar way by creating a SCRIPT element. Similar to Google Analytics, it gets a list of SCRIPT elements already in the page, and chooses the last one in the list. Then insertBefore is used to insert the new dynamic SCRIPT element into the document.
Normally, this is when the script would start downloading (asynchronously), but in this case the src hasn’t been set. Instead, the script’s URL has been put inside a string of JavaScript code that does a document.write of a SCRIPT HTML tag. (If you weren’t nervous before, you should be now.) (And there’s more.) Collective Media creates a global function called, of all things, appendChild. The dynamic SCRIPT element and string of document.write code are passed to this custom version of appendChild, which injects the string of code into the SCRIPT element, causing it to be executed. The end result, after all this work, is an external script that gets downloaded in a way that blocks the page. It’s not even asynchronous!
I’d love to see Collective Media clean up their code. They’re so close to making it asynchronous and improving the page load time of anyone who includes their ads. But really, doesn’t this entire blog post seem surreal? To be discussing this level of detail and optimization for something as simple as adding a script element dynamically is a testimony to the complexity and idiosyncrasies of the DOM.
In threads and discussions about adding simpler behavior to the browser, a common response I hear from browser developers is, “But site developers can do that now. We don’t have to add a new way of doing it.” Here we can see what happens without that simpler behavior. Hundreds, maybe even thousands of person hours are spent reinventing the wheel for some common task. And some dev teams end up down a bad path. That’s why I’ve proposed some clarifications to the ASYNC and DEFER attributes for scripts, and a new POSTONLOAD attribute.
I’m hopeful that HTML5 will include some simplifications for working with the DOM, especially when it comes to improving performance. Until then, if you’re loading scripts dynamically, I recommend using the latest Google Analytics pattern or the jQuery pattern. They’re the most bulletproof. And with the kinds of third party content I’ve seen out there, we need all the bulletproofing we can get.