Hello, fellow web perf enthusiast! Contribute to the 2024 edition happening this December. Click.

Web Performance Calendar

The speed geek's favorite time of year
2011 Edition
ABOUT THE AUTHOR

Stoyan (@stoyanstefanov) is a Facebook engineer, former Yahoo!, writer ("JavaScript Patterns", "Object-Oriented JavaScript"), speaker (JSConf, Velocity, Fronteers), toolmaker (Smush.it, YSlow 2.0) and a guitar hero wannabe.

JavaScript downloads block the loading of other page components. That’s why it’s important (make that critical) to load script files in a non-blocking asynchronous fashion. If this is new to you, you can start here or here.

In this post I’ll examine the topic from the perspective of a third party – when you’re the third party, providing a snippet for other developers to include on their pages. Be it an ad, a plugin, widget, visits counter, analytics, or anything else.

Let’s see in much detail how this issue is addressed in Facebook’s JavaScript SDK.

The Facebook plugins JS SDK

The Facebook JavaScript SDK is a multi-purpose piece of code that lets you integrate Facebook services, make API calls and load social plugins such as the Like button.

The task of the SDK when it comes to Like button and other social plugins is to parse the page’s HTML code looking for elements (such as <fb:like> or <div class="fb-like">) to replace with a plugin. The plugin itself is an iframe that points to something like facebook.com/plugins/like.php with the appropriate URL parameters and appropriately sized.

This is an example of one such plugin URL:
https://www.facebook.com/plugins/like.php?href=bookofspeed.com&layout=box_count

The JavaScript SDK has a URL like so:

http://connect.facebook.net/en_US/all.js

The question is how do you include this code on your page. Traditionally it has been the simplest possible (but blocking) way:

<script src="http://connect.facebook.net/en_US/all.js"></script>

Since day one of the social plugins though, it has always been possible to load this script asynchronously and it was guaranteed to work. Additionally, a few months ago the async snippet became the default when SDK snippet code is being generated by the various wizard-type configurators.

Here’s how an example configurator looks like:

The async code looks more complicated (it’s longer) than the traditional one, but it’s well worth it for the overall loading speed of the host page.

Before we inspect this snippet, let’s see what some of the goals were when designing a third-party provider snippet.

Design goals

  • The snippet should be small. Not necessarily measured in number of bytes, but overall it shouldn’t look intimidating.
  • Even though it’s small, it should be readable. So no minifying allowed.
  • It should work in “hostile” environments. You have no control over the host page. It may be a valid XTHML-strict page, it may be missing doctype, it may even be missing (or have more than one) <body>, <head>, <html> or any other tag.
  • The snippet should be copy-paste-friendly. In addition to being small that means it should just work, because people using this code may not even be developers. Or if developer, they may not necessarily have the time to read documentation. That also means that some people will paste that snippet of code many times on the same page, even though the JS needs to be loaded only once per page.
  • It should be unobtrusive to the host page, meaning it should leave no globals and other left overs, other than, of course, the included JavaScript.

The snippet

The snippet in the Facebook plugin configurators looks like so:

<script>(function(d, s, id) {
  var js, fjs = d.getElementsByTagName(s)[0];
  if (d.getElementById(id)) return;
  js = d.createElement(s); js.id = id;
  js.src = "//connect.facebook.net/en_US/all.js#xfbml=1";
  fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>

Let’s see what’s going on here.

On the first and last line you see that the whole snippet is wrapped in an immediate (aka self-invoking, aka self-executing) function. This is to assure that any temporary variables remain in the local scope and don’t bleed into the host page’s global namespace.

On line 1 you can also see that the immediate function accepts three arguments, and these are supplied on the last line when the function is invoked. These arguments are shorthands to the document object and two strings, all of which are used more than once later in the function. Passing them as arguments is somewhat shorter than defining them in the body of the function. It also saves a line (vertical space), because the other option is something like:

<script>(function() {
  var js, fjs = d.getElementsByTagName(s)[0],
      d = document, s = 'script', id = 'facebook-jssdk';
  // the rest...
}());</script>

This would be one line longer (remember we want readable snippet, not overly long lines). Also the first and the last line will have “unused” space as they are somewhat short.

Having things like the repeating document assigned to a shorter d makes the whole snippet shorter and also probably marginally faster as d is local which is looked up faster than the global document.

Next we have:

var js, fjs = d.getElementsByTagName(s)[0];

This line declares a variable and finds the first available <script> element on the page. I’ll get to that in a second.

Line #3 checks if the script isn’t already on the page and if so, exits early as there’s nothing more to do:

if (d.getElementById(id)) return;

We only need the file once. This line prevents the script file from being included several times when people copy and paste this code multiple times on the same page. This is especially bad with a regular blocking script tag because the end result is something like (assuming a blog post type of page):

<script src="...all.js"></script>
<fb:like /> <!-- one like button at the top of the blog post -->
 
<script src="...all.js"></script>
<fb:like/> <!-- second like like button at the end of the post -->
 
<script src="...all.js"></script>
<fb:comments/> <!-- comments plugin after the article -->
 
<script src="...all.js"></script>
<fb:recommendations/> <!-- sidebar with recommendations plugin -->

This results in a duplicate JavaScript which is all kinds of bad, because some browsers may end up downloading the file several times.

Even if the JavaScript is asynchronous and even if the browser is smart enough not to reparse it, it will still need to re-execute it, in which case the script overwrites itself, redefining its functions and objects again and again. Highly undesirable.

So having the script with an id like 'facebook-jssdk' which is unlikely to clash with something on the host page, lets us check if the file has already been included. If that’s not the case we move on.

Next line creates a script element and assigns the id so we can check for it later:

js = d.createElement(s); js.id = id;

Next line sets the source of the script:

js.src = "//connect.facebook.net/en_US/all.js#xfbml=1";

Note that the protocol of the URL is missing. This means that the script will be loaded using the host page’s protocol. If the host page uses http://, the script will load faster, and if the page uses https:// there will be no mixed content security prompts.

Finally we append the newly created js element to the DOM of the host page and we’re done:

fjs.parentNode.insertBefore(js, fjs);

How does that work? Well, fjs is the first (f) JavaScript (js) element available on the page. We grabbed it earlier on line #2. We insert our new js> element right before the fjs. If, let’s say, the host page has a script element right after the body, then:

  • fjs is the script
  • fjs.parentNode is the body
  • our new script is inserted between the body and the old script

Appending alternatives

Why the trouble with the whole parentNode.insertBefore? There are simpler ways to add a node to the DOM tree, like appending to the <head> or to the <body> using appendChild(), however this is the way that is guaranteed to work in nearly all cases. Let’s see why the others fail.

Here is a common pattern:

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

Or a variation if document.head is available in newer browsers:

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

The problem is that you don’t control the markup of the host page. What if the page doesn’t have a head element? Will the browser create that node anyways? Turns out that most of the times, yes, but there are browsers (Opera 8, Andriod 1) that won’t create the head. Here is the test case by Steve Souders with the browserscope results.

What about the body? You gotta have the body. So you should be able to do:

document.body.appendChild(js);

I created browserscope test and couldn’t find a browser that will not create document.body. But there’s still the lovely “Operation Aborted” error which occurs in IE7 when the async snippet script element is nested and not a direct child of the body.

Last chance:

document.documentElement.firstChild.appendChild(js)

document.documentElement is the HTML element and its first child must be the head. Not necessarily, as it turns out. If there’s a comment following the HTML element, WebKits will give you the comment as the first child. Here’s an investigation with a test case.

Whew!

Despite the possible alternatives, it appears that using the first available script node and insertBefore is the most resilient option. There’s always going to be at least one script node, even if that’s the script node of the snippet itself.

(Well, “always” is a strong word in web development. As @kangax pointed out once, you can have the snippet inside a <body onload="..."> and voila – magic! – a script without a script node)

What’s missing?

You may notice some things missing in this snippet that you may have seen in other code examples.

For instance there are none of:

js.async = true;
js.type = "text/javascript";
js.language = "JavaScript";

These are all defaults which don’t need to take up space, so they were omitted. Exception is the async in some earlier Firefox versions, but the script is already asynchronous and non-blocking enough anyway.

Same goes for the <script> tag itself. It’s an HTML5-valid bare bone tag with no type or language attributes.

First parties

This whole discussion was from the perspective of a third-party script provider. If you control the markup, some things might be different and easier. You can safely refer to the head because you know it’s there. You don’t have to check for duplicate insertions, because you’re only going to insert it once. So you may end up with something much simpler, such as:

<script>(function(d) {
  var js = d.createElement('script');
  js.src = "http://example.org/my.js";
  (d.head || d.getElementsByTagName('head')[0]).appendChild(js);
}(document));</script>

This is all it takes when you control the host page.

Also we assumed all the time that whenever the script arrives, it just runs. But you may have different needs, for example call a specific function once the script is ready. In which case you need to listen to js.onload and js.onreadystatechange (example). In even more complex examples, you may want to load several scripts and guarantee their order of execution. At this point you may want to look into any of the available script loader projects such as LAB.js or head.js which are specially designed to solve these cases.

Parting words / on the shoulders of giants

It’s a little disturbing that we, the web developers, need to go to all these lengths to assure an asynchronous script execution (in a third-party environment or not). One day, with a few dead browsers behind us, we’ll just be able to say script async=true and it will just work. Meanwhile, I hope that this post will alleviate some of the pain as a resource to people who are yet to come to this problem and will hopefully save them some time.

Google AdSense folks have gone through a lot of trial and error while sharing with the community their progress, Mathias Bynens also wrote an inspirational critique of their snippet. Steve Souders has done research and written about this topic, also MSN.com was probably among the first to use such a technique for loading JavaScript. There are writeups from Yahoo and many other on the topic. These are some of the giants that have helped in the search of the “perfect” snippet. Thank you!

(pssst, and if you see something that is less than perfect in the snippet, please speak up)