The ridiculous case of adding a script element

Adding a script element to a page should be a no-brainer. Yet, it's ridiculously unreliable in the wild - when you don't have any idea of the surrounding markup.

You know the drill - create a script element, point its src to a URL and add it to the page so that script file can be downloaded in a non-blocking manner.

Creating the script is nice and easy:

var js = document.createElement('script');
js.src = 'myscript.js';

The problem is how to add it to the page.

1. to the head

Probably the most common approach is to append to the head of the document:

document.getElementsByTagName('head')[0].appendChild(js);

You get all head elements (there should be only one) and you add the script there so the result is

<html>
  <head>
    <script>
    ...

But what if there's no head in the document? Remember, we want solid robust code that always works.

Well turns out that most browsers will create the head element even if the tag is not there.

Most, but not all as Steve Souders' browserscope test shows. Exceptions include browsers like Opera 8 but also Android 1.6 and one iPhone 3 - hardly old and negligible.

So head is out.

2. add to the body

This is even shorter:

document.body.appendChild(js);

what if there's no <body> tag? Well my test shows that all tested browsers will create a body element. Even one that has a working appendChild() method. Now, while some of those that don't create head, do create body, I'm still a little uncomfortable that I couldn't find a single browser that doesn't create body. Makes me feel a little worried about the testing and data collecting.

But even if we assume that in all browsers, document.body always exist, there's still a problem. IE7 and the dreaded "Operation aborted" error.

If you modify BODY while loading the page and from a script that is not a direct child of body, but nested in another element, you get Operation aborted and nothing works.

Surprisingly simple page fails miserably:

<html>
  <body>
      <div>
          <script>
          var js = document.createElement('script');
          js.async = true;
          js.src = "http://tools.w3clubs.com/pagr/1.sleep-2.js";
          document.body.appendChild(js);
          </script>
      </div>
  </body>
</html>

See it live in IE7 and be amazed

Now operation aborted may be solved with a defer, but maybe not in IE9.

Another reason not to attempt anything with body is if the script is included in the head. At execution time we have not reached <body> yet, so document.body doesn't exist. Demo.

3. use documentElement

document.documentElement is the html doc itself. Now that's got to exist no matter what.

So you go like

var html = document.documentElement;
html.insertBefore(js, html.firstChild);

And it works!

But what if the firstChild is a comment before the head? This makes something kinda weird:


<html>
  <script>
  <!-- comment -->
  <head>
    ..

Script obviously has no place there, but my test page worked in ie6789, and recent versions of FF, Chrome, Safari, Opera.

But looks like there are other browsers where this comment thing fails as reported(btw, good to read the whole post and all the comments) by Google Analytics folks who say they have received complaints when they used to do that. There might be other cases or browsers I didn't try. So reluctantly, we move on.

4. hook to the first script

Ahaa, if you're running a script, then this script must either be inline <script>bla</script> or external <script src="meh.js">. Either way, there's gotta be at least one script tag! Wo-hoo!

So the final solution is:

var first = document.getElementsByTagName('script')[0];
first.parentNode.insertBefore(js, first);

No matter where the first script might be, we glue out new one right above it.

Drawback is that looking for script nodes might be a little more expensive than looking for document.documentElement or document.body or the single match of getElemenetsByTagName('head')

There's still a case when there might not be a script element at all. Ah, impossible, since we're running a script there must be a script element! Well (and thanks to @kangax who pointed this to me while he was reviewing JavaScript Patterns!) here's one example:

<body onload="alert('Look ma, executing script without a script tag!')">

Overall while not completely foolproof, this is as close as you can get to being able to achieve the simple task - add a new script node. Isn't web development just magnificent?

(Funny thing if you use this only to load a script asynchronously: in order to load a script (file), you need a script that refers to a script, possibly itself. JavaScript all around.)

Once again the whole snippet:

var js = document.createElement('script');
js.src = 'myscript.js';
var first = document.getElementsByTagName('script')[0];
first.parentNode.insertBefore(js, first);

15 Responses to “The ridiculous case of adding a script element”

  1. Andrea Giammarchi Says:

    What I usually do is:

    var html = document.documentElement;
    html.insertBefore(js, html.lastChild); // NOTE: lastChild

    The DOM is a stack of nested elements, and as is for every stuck, it’s better to move as less “indexes” as possible.
    A script before script 0 moves all other elements, a script before last node moves last node only at the end.
    Whenever this brings real benefits or not, it always worked for me and it’s more linear/logic.

    As example, the script itself that is relying into document.getElementsByTagName(“script”)[0] could be outside the header but it will still work.

    Does it really matter then since apparently there are no “official” or “unavailable places” for scripts? :-)

  2. stoyan Says:

    nice, thanks! in general makes a lot of sense about top vs bottom of the stack

    so your example makes something like:

    <html>
      <head>
      <script>
      <body>
    
  3. Adam Bankin Says:

    Is there a reason why you don’t create and append a node as the firstChild of documentElement if a node doesn’t exist?

  4. Adam Bankin Says:

    That was meant to say “head” but I used greater and less thans

  5. John-David Dalton Says:

    The “Operation Aborted” problem affects IE 5.5 – 8 (I’ve not run into it with IE8 but MS’s docs say so).
    For more info check out Microsoft’s knowledge base posts 961712 and 974322.

  6. Mathias Bynens Says:

    Great overview!

    You don’t actually need the .async = true for dynamically inserted scripts; see http://mathiasbynens.be/notes/async-analytics-snippet#async for more information. (That page explains the snippet I always use to dynamically load scripts.)

  7. Link-urile săptămânii 5-11 septembrie | Staicu Ionuţ-Bogdan Says:

    [...] The ridiculous case of adding a script element. [...]

  8. Andrea Giammarchi Says:

    yes Stoyan, something like that … but being this a valid layout for all browsers:

    I really don’t think we should care that much about the position, as long as it works as expected

  9. Andrea Giammarchi Says:

    <!doctype html>
    <script src=”whatever.js”></script>

  10. Diego Perini Says:

    If the objective is just to avoid the IE bug, in your example change:

    document.body.appendChild( js );

    with:

    document.body.insertBefore( js, document.body.lastChild );

    I still prefer to have all my scripts in the head section when in control of the sources, if that is not the case prefer using the ‘document.documentElement’ as a reference for insertion.

    The ‘root’ element is the only element that is always present in the DOM when Javascript execution starts, even for different document types (XHTML/XML).

    In my tests, script tags works when added to the ‘documentElement’, even if its first child is a TextNode. Even if the above modification make your sample work I would still suggest not to do it so and use ‘documentElement’ instead, more so for IE.

  11. satellite TV Says:

    satellite TV…

    [...]JSPatterns.com » Blog Archive » The ridiculous case of adding a script element[...]…

  12. page Says:

    godd u really suck

  13. Sam Says:

    don’t forget to get back your windows tax when buying a new computer (60 – 70$).

    ex: acer.custhelp.com/app/answers/detail/a_id/280

  14. Acheter Vimax Says:

    This site leaves a constructive materials that may possibly ‘t be disregarded primarily it includes and exhibits us one other level of hugely amazing impression.

  15. 黄色的鸭子 | Spencer Says:

    [...] 不仅采用异步的方式加载analytics代码,并且细致考虑了代码加载后的位置。最大化优化了网站的性能(why?答案在这里)。 [...]

Leave a Reply