JavaScript downloads block the loading of other page components. That’s why it’s important (make that critical) to load script files in a nonblocking asynchronous fashion. If this is new to you, you can start with this post on the Yahoo User Interface (YUI) library blog (http://www.yuiblog.com/blog/2008/07/22/non-blocking-scripts/) or the Performance Calendar article (http://calendar.perfplanet.com/2010/the-truth-about-non-blocking-javascript/).
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 plug-in, 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 JavaScript SDK is a multipurpose piece of code that lets you integrate Facebook services, make API calls, and load social plug-ins such as the Like button (https://developers.facebook.com/docs/reference/plugins/like/).
The task of the SDK when it comes to Like button and other social
plug-ins is to parse the page’s HTML code looking for elements (such as
<fb:like>
or <div class="fb-like">
) to replace with a
plug-in. The plug-in 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 plug-in 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 plug-ins 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.
Figure 4-1 shows 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.
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 they are developers, 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 leftovers, other than, of course, the included JavaScript.
The snippet in the Facebook plug-in 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>
Take a look at what’s going on here.
On the first and last line you see that the whole snippet is wrapped in an immediate (a.k.a., 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 whether 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 (http://developer.yahoo.com/performance/rules.html#js_dupes), 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.
The next line creates a script
element and assigns the ID so we can check for it later:
js
=
d
.
createElement
(
s
);
js
.
id
=
id
;
The following 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.The new script is inserted between the
body
and the oldscript
.
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>
by 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, Android 1) that won’t create the head. A
BrowserScope test by Steve Souders demonstrates this (http://stevesouders.com/tests/autohead.html).
What about the body
? You gotta
have the body. So you should be able to do:
document
.
body
.
appendChild
(
js
);
I created a browserscope test (http://www.phpied.com/files/bscope/autobody.html) 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. There’s an investigation with a
test case that show this (http://robert.accettura.com/blog/2009/12/12/adventures-with-document-documentelement-firstchild/).
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
(http://twitter.com/kangax) pointed out once, you
can have the snippet inside a <body
onload="...">
and voila—magic!—a script without a script
node.)
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 nonblocking and
asynchronous enough anyway.
Same goes for the <script>
tag itself. It’s an HTML5-valid bare-bones tag with no type
or language
attributes.
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: http://www.phpied.com/javascript-include-ready-onload/). 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 (http://labjs.com/) or head.js (http://headjs.com/) which are specially designed to solve
these cases.
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 be able to simply 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 their progress with the community, and Mathias Bynens also wrote an inspirational critique (http://mathiasbynens.be/notes/async-analytics-snippet) of their snippet. Steve Souders (http://stevesouders.com/) has done research and written about this topic, and MSN.com was probably among the first to use such a technique for loading JavaScript. There are writeups from Yahoo and many others on the topic. These are some of the giants that have helped in the search of the “perfect” snippet. Thank you!
(Psst, and if you see something that is less than perfect in the snippet, please speak up!)
Note
To comment on this chapter, please visit http://calendar.perfplanet.com/2011/the-art-and-craft-of-the-async-snippet/. Originally published on Dec 04, 2011.
Get Web Performance Daybook Volume 2 now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.