appendChild vs insertBefore

May 11, 2010 12:15 am | 43 Comments

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.

43 Responses to appendChild vs insertBefore

  1. Why are all of these scripts adding to the , shouldn’t they be adding to the instead so as to let the page load CSS/HTML in parallel (non blocking fashion)?

    I use the following code to dynamically add to the tag…

    document.getElementsByTagName(“body”)[0].appendChild(script);

  2. Sorry, in my previous comment the words <head> and <body> were stripped out?

    It should have said:

    Why are all of these scripts adding to the <head>, shouldn’t they be adding to the <body> instead so as to let the page load CSS/HTML in parallel (non blocking fashion)?

    I use the following code to dynamically add to the tag…

    document.getElementsByTagName(“body”)[0].appendChild(script);

  3. Steve,

    I don’t think it’s surreal at all.

    I developed my own method of loading Google Analytics (and others) asynchronously a couple of years ago. Much to the delight of my customers, that I could create a rich experience on their sites and still keep it fast and responsive.

    In the end, it’s all about make the users happy … and not having a slow website, does not detract from their happiness.

  4. @Mark: adding something to body is slower than the same to head. Also document.body can be used for this purpose.
    Anyway document.documentElement.firstChild looks like the fastest way to get a DOM node to insert a CSS/JS chunk.

  5. I wonder why google doesn’t use protocol relative urls like you have showed before? That should avoid the need to check if its http or https right?

  6. @sunnybear thanks for the info, much appreciated. Are there any documented bench tests as to which is faster, adding to body/head? Just so I could get a better idea of what’s happening there.

  7. When teaching these techniques, please mention that scripts loading via any of these async mechanisms MUST NOT call document.write(), because doing so blows away the document in IE and Firefox trunk and causes timing-dependent behavior in shipped Firefox and in WebKit.

  8. I’m assuming also that if I did want to append to the body rather than the head (for whatever reason) then when used in a HTML document the document.documentElement.lastChild *should* always refer to the body, correct? (I’m quite sure it would do, I can’t think of any reason why it wouldn’t be).

  9. Sorry if this is a stupid question but is it correct in thinking that adding a script to the head wont have any negative performance implications?

    Because if you have your initial script tag (the one that does the dynamic loading of the other scripts) at the bottom of the body then this will prevent blocking of the rest of the HTML/CSS, so then dynamically adding a script tag to the head wont block anything because the page has effectively already finished loading – correct?

  10. Steve,

    Very good analysis of Collective tags, that said the comparison between their ad tags and Google analytics is not fair.

    Google Analytics delivers and controls the JavaScript code of the script inserted via the technique mentioned above. They will make sure that will not use any javascript or code that would break under that technique – like document.write.

    On the other side adserving systems or adnetworks, like Collective or even Google’s DoubleClick solution – cannot control what is delivered in the script they are embeding – simply because they might deliver ads to other ad networks/adserving systems which could include JavaScript code that would break the page (example contain document.write). As a result all adserving systems rely on document.write, in order to make sure that the page does not break.

  11. @Morgan: I’m not saying it shouldn’t be done. I’m saying it’s bizarro world that we have to spend our time on these things, rather than writing logic and features.

    @Simon: One issue with protocol-less URLs is that stylesheets get downloaded twice in IE7 and 8. See my Missing schema double download post.

    @Henri: Yes, thanks for reminding us that async scripts and document.write don’t mix (hence my insistence on pushing ad networks away from document.write).

    @Mark: Dynamically adding to head does not cause the type of blocking issues you’re thinking of.

    @Tony: I’m not going to let the ad networks off that easy. They are responsible for the content they inject in web sites. I’m not saying it’ll be easy to fix the issues, but we definitely should not accept the current state of using document.write as the de facto standard.

  12. Steve,

    Not arguing to let adnetworks get easy of the hook with the crap they do. I am simply arguing you cannot compare apples (analytics) to oranges (adserving).

    It is clear the ad networks are impacting performance and they do a lot of things that are bad. I am all for pushing them to fix their mistakes.

    Collective obviously has plenty of mistakes in their code and they need to solve them. That said the key problem you listed with the inline JavaScript is complicated in this case because an adserver/adnetwork cannot control what is in the chain of ads they deliver – and moving to dynamic script insertion could make things worst.

    The main reason is that all adserving systems (and some ad networks) allow their customers to input the HTML/JS for the ad, which is in turn delivered by the adserver through the ad tags on the page. The adserver cannot modify the code on the fly to remove document.write and anything else that would break the dynamic insertion of JS. The code of the ad has to be from the start coded with in mind that the tag will be appended dynamically! Hence the use of document.write

    I really admire your drive to fix the problem, and I think you are in a good position to do so since you work for the biggest ad network and biggest adserver company out there! Why don’t you repeat what you did with the Google Analytics with the DoubleClick division – which if i do not mistake is using document.write on their tags and at the same time has relationships of some sort with all adnetworks out there.

  13. Steve,

    Are you going to officially recommend a best practice?

  14. From the article:

    “It turns out that not all web pages have a HEAD tag, and not all browsers will create one when it’s missing.”

    What browsers are we talking about here

  15. @Lenny: The GA and jQuery patterns both look solid.

    @Andy: I didn’t conduct those tests. We could do that in Browserscope with the new “create your own test” capability. I’ll try that now.

  16. Hi, I’m using this:

    var element = document.createElement(“script”);
    element.src = “deferredfunctions.js”;
    document.body.appendChild(element);

    from: http://code.google.com/speed/page-speed/docs/payload.html#DeferLoadingJS

  17. @Andy: Safari on iPhone won’t create a HEAD if it isn’t in the page.

  18. @Mark – appending to the document body while the page is loading can result in an “operation aborted” error in IE: http://www.nczonline.net/blog/2008/03/17/the-dreaded-operation-aborted-error/. That’s why a lot of scripts have moved away from it.

    I’m curious as to why the Google Analytics team went with searching for script tags rather than inserting before the first node in the body:

    var body = document.body;
    body.insertBefore(scriptNode, body.firstChild);

    This would save the overhead of searching, while avoiding the issues with HEAD and the operation aborted error.

  19. @Nicholas – As Steve mentioned, GA is used on a large variety of pages. body.firstChild doesn’t work if there is no body. We used the gEBTN(‘script’)[0] method to try to be as foolproof as possible.

  20. I documented my optimized version of the asynchronous Google Analytics snippet on my blog.

    @Steve, I’d like to get back to you on your reply to Simon:

    @Simon: One issue with protocol-less URLs is that stylesheets get downloaded twice in IE7 and 8. See my Missing schema double download post.

    I don’t understand how that’s relevant. Surely that’s not a reason to avoid protocol-less URIs for JavaScript files, or is it?

  21. @Nicholas thanks for that info, that’s really useful to know! :)

    @Brian much like your feedback regarding Safari on iPhone not creating a HEAD tag, have you any info on when you’ve seen a page without a BODY tag?

    Seems like Google’s way of searching for the SCRIPT tag is most robust but could be quite slow depending on size of HTML in page. Whereas Nicholas provides a quicker solution that avoids issues with Safari on iPhone but in itself could be problematic for pages that have no BODY.

    Personally I’m going to stick with document.documentElement.firstChild as the scripts I write are for sites we have access to and can correct if they are missing HEAD/BODY elements, whereas for Google I can appreciate why they look for the SCRIPT tag because they don’t know the environment their code will be used in.

  22. Wouldn’t it be possible to download the script with xmlhttprequest and then eval() it?

    Btw, is insertBefore really working in IE? IIRC it didn’t even exist last time i tried to use it in IE8 with IE7 compatibility mode.

  23. @Erik yes you can use XHR but the execution order wont be preserved.

    See both:

    http://stevesouders.com/hpws2/couple-xhr-eval.php and http://stevesouders.com/hpws2/couple-xhr-injection.php?t=1273663871)

  24. @Mark The order is not preserved with script appending either.

    See http://stackoverflow.com/questions/2804212/dynamic-script-addition-should-be-ordered
    “However looking at the resource loading view, chrome seems to parse[execute] whichever is returned first from the server, while firebug always loads them in the order the script tags were added, even when B is returned first from the server.”

    With xhr you actually have more control over the order because you can manually call the eval whenever you want. Doesn’t have to be done as soon as the download is complete.

  25. Shouldn’t we put more time towards promoting script loading that doesn’t delay the onload event? Check the last example here: http://friendlybit.com/js/lazy-loading-asyncronous-javascript/

  26. @Mark – One of the issues we hit with documentElement.firstChild was that the firstChild may be a comment above the head.

    As for document.body.firstChild, I had forgotten last night that in addition to not working when there is no body, it also doesn’t work when used in the head. See the following example:

    http://analytics.nfshost.com/nobody.html

  27. @Erik Personally I use a callback function with the Script DOM Element technique as it means I can control the order of execution, but yes I guess being you have control over when to eval you can decide the order. I prefer the callback method with the Script DOM Element technique – everyone to their own :)

    @Brian ok, so no clear cut solution so I’m going with Nicholas C. Zakas solution as I know I can control the body, but if an unknown environment I’ll end up with Google’s SCRIPT variation.

  28. We managed to trigger an “operation aborted” bug in IE7 and IE8 as well, which had to do with inserting multiple asynchronous scripts into the document (and just the right timing). Changing all of them to insertBefore instead of appendChild fixed the issue.

    (Many thanks to Brian Kuhn for helping us try to diagnose it).

  29. I came across this again which was useful:

    http://stevesouders.com/efws/images/0405-load-scripts-decision-tree-04.gif

  30. @Emil with regards to the link you provided: the onload event can be very slow if you have a page with lots of images.

    If for example you have elements that need to be interacted with that utilise JavaScript to provide certain AJAX functionality (as a random example) then waiting for the load event could leave your users waiting and clicking for something to happen.

    Whereas if the script runs when the DOM is finished being parsed then the interaction can happen a lot more quickly and give the user a perceived performance improvement.

    The onload event is fine though for scripts that aren’t critical to the page (such as advertisement banners for example).

  31. I also hit the issue that Brian Kuhn hit: document.documentElement.firstChild could be an HTML comment. Firefox skips over these, but other browsers don’t.

  32. @Andy: I created a Browserscope user test to test this and wrote a blog post about it: AutoHead – my first Browserscope user test. The results? Android 1.6, Chrome 5.0.307 (strange), iPhone 3.1.3, Nokia 90, Opera 8.50, Opera 9.27, and Safari 3.2.1 do NOT automatically create a HEAD element. This is probably over 1% of your traffic, so is a valid concern.

  33. Latest Gecko and Webkit based browsers support downloading assets in parallel while still preserving execution order. But using async attribute breaks that behavior in FF and forces us to wait for dependencies before downloading others.

  34. @Erik The article was talking about cross-domain, which is quiet messy and most of the time not even allowed by the browser:

    http://www.nczonline.net/blog/2010/05/25/cross-domain-ajax-with-cross-origin-resource-sharing/

  35. A plain vanilla script element in the HTML source works, and is simple too!

    Performance hits using large-ish and inefficient 3rd libraries (and I’ve noticed you seem to have a penchant for one) can be avoided by just not using those libraries.

    Glad to see insertBefore catching on as a safer alternative to appendChild.

    I don’t know why it’s taken so long or why MS has never mentioned using insertBefore as an alternative. That idea was proposed that on c.l.js over two years ago (about a month before Zakas’ blog post, to be fair)[1]. A better explanation of why it works is here:
    http://groups.google.nr/group/comp.lang.javascript/msg/ea89839a21eadf84

    though I recommend reading the whole thread, cause it’s a good one:
    http://groups.google.nr/group/comp.lang.javascript/browse_thread/thread/2206b18ab04ba2c5/e31c9040fe0f108c?lnk=raot

    A problem with using documentElement.firstChild is that if there’s a comment after the HTML start tag, then it should be a comment node and appending to a comment node is probably going to result in errors. More was explained here:

    http://github.com/jquery/jquery/commit/b8076a914ba9d400dc9c48d866b145df6fabafcf#all_commit_comments

    [1] http://groups.google.com/group/comp.lang.javascript/msg/3203bb04e128b602?hl=en&&q=%2BinsertBefore

  36. @steve
    The GA tag knows there is at least one script element on the page so can confidently search for that. If it was an image being dynamically inserted rather than script, then a search for script would still be ok, but if the first script element found was in the head section (as it likely would be) and the image was therefore inserted there, would the browser still fetch the image when its src attribute was dynamically set? A quick test on a couple of browsers shows that the image is still fetched, but what is the point of the browser doing that given it doesn’t render images from the head section?

  37. @Paul: Many (most?) web pages contain image “beacons” (some people call them “tags”). These are requests for images that are not shown in the page and typically aren’t even in the DOM. The purpose is to log information back to the server. Examples are for counting ad impressions and tracking pages.

  38. @steve – yes, and what I was trying to say is if that beacon/tag, which is a 1×1 image say, is added to the head section, and the image src property is then set dynamically, will the browser bother to fetch the image from the location specified by the image’s src property? For example:

    var theImage = document.createElement(“img”);
    var s = document.getElementsByTagName(‘script’)[0];
    // Let’s say that the first script element on the page
    // is in the head section, which is likely, so the image
    // inserted into the page by the line below will also
    // be in the head section…
    s.parentNode.insertBefore(theImage, s);
    // If the image had been added to the body section, then
    // there would be no doubt that the line below would
    // result in the browser fetching the image from the
    // specified location. But my question was that since the
    // image is in the head section, is there a possibility
    // that the browser would regard fetching the image as
    // unnecessary (as images in the head section are not
    // displayed on the web page)?
    theImage.src=’some image location’;

  39. @Paul: yes

  40. I curse the web that I even have to read this sort of sordid detail to build “solid” web pages (if there is such a thing).

    Excellent post and comments. Thanks.

  41. @Mathias GA can’t do a protocol-relative URL because they use different subdomains for secure vs. non-secure pages. I don’t know why they do this though. Possibly something to do with cookies, but I would assume both are stateless domains so wouldn’t respond to cookies differently.

  42. Very interesting article, I was, for a lot of time, simply using document.write, then one day, I discovered how to set a timeout ( in order to check that document.body is loaded ) before appeding the document.writes in it ..

    There were lots of unguided experimentations which resulted in my “own crappy js framework”, thanks to you I’ve build that function in order to make sure underscore framework is loaded ( through my function wait() which evals the function in argument once the body is loaded ) and I don’t know if I’m doing right here ..

    if(typeof(_)==’undefined’){
    var ds=document.createElement(‘script’);ds.type=’text/javascript’;ds.async=true;ds.src=’/underscore.js’;ds.onload=function(){wait(‘w1()’);}
    document.getElementsByTagName(‘head’)[0].appendChild(ds);}

    Here’s my weirdest code for document.write ( non-revised since 2 years as I’m lazy )

    document.write=function(s){xx(s);}/
    function xx(s){if(!s)return;
    div=document.createElement(“div”);div.innerHTML=s;nodes=div.childNodes;pos=document;
    while(pos.lastChild && pos.lastChild.nodeType==1){pos=pos.lastChild;}
    try{pa=pos.parentNode;}catch(e){try{pa=pos.parent;}catch(e){}}if(pa==null)pa=prevpa;
    pnd=pa.nodeName;
    if(pnd==’BODY’){
    if(document.body==null&&!$bodyloaded){if($i>20)return;$i++;$db+=”xx:wait;”;setTimeout(function(){$i++;xx(s);},50);return;}//Retries writing till document.body is defined
    $bodyloaded=1;
    prevpa=pos=prev=document.body;while(nodes.length)pa.appendChild(nodes[0]);
    return;
    }
    if(pa==null||pa==undefined||pnd==’#document’||pnd==’BODY’||pnd==’HTML’)pos=prev;
    prevpa=pa;prev=pos;while(nodes.length)pa.appendChild(nodes[0]);
    }

  43. For normal delivery the mother should be fit to cope
    with the delivery. The fitness vacation – Whenever vacation is
    referred to, most of would think enjoying tropical foods, sitting beside the pool,
    drinking, and partying all night. Smoking can affect your health and fitness since it affects
    all the bodily functions and damages vocal cords
    and skin.

    My page – Annett