[This post is mirrored from the 2012 Performance Calendar]
In days of yore
Way back when, in the early days of web performance (ca. 2005), the suggested way to embed third party scripts in web
pages, was to use a straight up
<script> node. This was simple, and what happened next was
deterministic. The rest of the page blocked until this script node had loaded and executed.
This meant that if the script loaded successfully, any variables and functions defined in it, were available in the page immediately after. This made code easier to write. It also made for a terrible user experience.
Dynamic Script Nodes
Enter dynamic script nodes. Dynamic script nodes were not a new concept. We’d been using dynamic script nodes as a way
to do JSON-P based remoting for a while.
The concept was simple. Create a script node at run time, and set its
src attribute to the script you need loaded.
The browser downloads the script in the background, and once downloaded, executes it using the regular
The background downloading is key. It meant that subsequent parts of the page didn’t block, waiting for the script to download and execute. This was great for user experience, but made code harder to write. You could no longer expect variables and functions defined within the script to be available after you’d initiated loading the script. In fact, there was no reliable, cross-browser way to know when this script had completed loading.
Three methods started to appear.
- Callback functions
- Polling for variables or functions
- The method queue pattern
The first method required you to define a function in your main HTML file, and then let your dynamically loaded script node call that function. This limited how easily the script’s exposed API could be used. ie, there was no way to call methods exposed by the script before the script was loaded, and you’d have to hack around cases where the script failed to load.
Polling had all the problems of callbacks, and all the problems associated with polling in general (which is an entirely different topic altogether).
The Method Queue Pattern
The method queue pattern was an interesting development. In this pattern, the script, and users of the script agree
on a variable name. For example, google analytics uses the variable
_gaq. The API contract states that
users of the script should define an array with a particular name, we’ll call it
_mq for the rest of this
implement a queue, you always use the
push() method to add elements to the queue, and always use the
shift() method to take them off.
So to use the Method Queue Pattern (MQP for short), a user of the script creates an array and starts pushing method calls onto it as strings:
Each element pushed onto the queue is an array. The first element of the array is a string identifying the method while all subsequent elements are parameters to the method. The parameters can be of any datatype, including functions.
Once the script has completed loading, it looks for the array named
_mq, and starts reading and executing methods
off the queue. Once it has completed reading elements off the queue, it redefines the
method to directly execute the method rather than queueing it:
method3 method completes executing, it calls the callback function with its return value.
But we still block
At this point we have a way to download scripts asynchronously, call methods on the script without waiting for the script to finish downloading, and even get return values back from these methods through callbacks. We also don’t have to worry about our code throwing exceptions if the script doesn’t load (and if you’ve used progressive enhancement to build your site, it should still work perfectly).
Unfortunately, this isn’t the end of it.
It turns out that in most browsers, any resource that has started downloading before
onload, will block
onload. This means that if the script we loaded asynchronously was slow, or timed out, our
event would incur a significant delay. If your site does important tasks in the onload event (like load advertisements),
these might be delayed, and might never execute if the user leaves the page before that happens, causing a loss in
revenue. Every script added, whether directly or dynamically is a SPOF.
What we need is a way to download scripts asynchronously, without blocking the onload event, while still making its API available to code within the page.
One way to do this is to load the script itself in the onload event. Scripts loaded in the onload event do not block onload. However, it also means that none of the callback functions will execute until much after the onload event, and the script cannot perform any tasks that need to happen before onload (like measuring when the onload event fired, for example).
Who framed roger scriptlet?
A breakthrough was made in 2010, when the meebo team released the meebo bar. They noticed that an empty iframe wouldn’t block the onload event
of the parent page even if you add content to the iframe at a later time. To be specific, once a resource’s onload event has
fired, it is removed from the list of resources that block the page’s onload. An iframe’s onload event fires as soon as all
content within the iframe has loaded. An iframe whose
src attribute is set to
onload event fires immediately. [See note below]
The code is much larger than both, the simple script node and the dynamic script node code, but it’s completely non-blocking. The method has been called FIF, which stands for either Friendly IFrame, or Frame In Frame.
Of course, with this change comes added complexity. Our script now executes within an iframe, so all global variables
that our script looks for are within that iframe’s context. This is a problem since the
_mq array we
created is in the parent window. Additionally, we might face cross-domain issues.
I’ll address the cross-domain issues first.
The interesting thing about setting an iframe’s src attribute to
is if you check the value of
location.href inside an iframe with its src set to one of the above, you’ll get a
value exactly the same as the parent frame. The only difference that I know of between the two is that the first is
considered insecure content in IE6, so won’t work if your main page is over SSL.
With no other changes, the parent document and the iframe can communicate with each other without throwing a security exception.
There is however an issue if the main page sets
document.domain. This is true even
document.domain is set to itself (
document.domain is strange in that the state of a page (on IE at least) is different if this property is
set implicitly or explicitly. If set implicitly (ie, by the browser based on the current page’s domain), then all
other frames on that domain that also have it set implicitly can talk to each other. If set explicitly, however, then
all other frames on that domain must also set
document.domain explicitly to the same value in order to
communicate amongs themselves.
If the main page sets
document.domain, it makes it impossible for the main page to talk to our anonymous iframe,
which includes writing the content into that iframe. So, how do you change
document.domain on a page if you
cannot actually write any content into that page?
document.domain, but only if
document.domain were explicitly
set on the main page. We do this with the
try/catch block above.
try/catch block allows us to write content into the iframe, but on IE8 and below,
gets reset when we call
doc.open() inside the iframe. To get around that, we need to set
inside the iframe again just before adding our script node.
Also note that inside the onload handler,
this refers to the
document element. For some reason using
in there doesn’t quite work.
This has worked with every configuration that we can think of to test, but if you find something that breaks it, please let me know.
We also need to make a few changes to our script to get it to work from within the iframe, and since it’s likely that the script might be loaded synchronously as well, we still need to account for the non-iframe case.
Right at the top, our script needs to do this:
Notice a few things. We don’t use the
var keyword to declare the
_mq variable. This makes
sure it is declared global within the iframe. Secondly, we make sure
_mq inside the iframe is an alias of
_mq outside the iframe, and is set to a valid array object.
GLOBAL may be an alias either to
the current window or the parent window depending on whether the code is in an iframe or not.
Lastly, we check that a script node with an id of
js-iframe-async exists inside our iframe. This is
important because we need to distinguish between on one hand, our script running inside an iframe that we created and on
the other hand, our script running inside a page that is inside a larger iframe created by someone else. There are
other ways to determine this, but setting an id on our script node is easy to do.
There are a few more things to note about the script running in an iframe. If it needs to attach to any in page events,
or examine elements in the page, it needs to reference
GLOBAL.document instead of
document. Of course, you could use your own namespace instead of calling it
GLOBAL. For example, the boomerang library uses
We never shadow the
document objects because we may need to use them either from
the current frame or the parent depending on the use case.
For example, if boomerang needs to load additional plugins, it loads them using the
window object, but if it
needs to load in-page resources, it loads them using the
We’ve been running this code in production for a month now, with some sites using it via the iframe method and others using it via the dynamic script node method. There has been no noticeable difference in the number of beacons before and after switching the code over.
The non-blocking script loader pattern
So to summarise the pattern, this is what we do:
- Dynamically create an iframe with src set to
document.domainis set explicitly, then set it inside the iframe too.
- Set this script node’s id to
js-iframe-async, or anything fixed that you prefer
- Inside the script, check whether you’re running via the iframe pattern or not
- Create an alias to the global window object that points to the right window
- Create an alias to the global method queue array
- Do not shadow
The state today
At LogNormal, we’ve made changes to boomerang (the opensource version as well as the one we serve to our customers) to work with all three loading patterns. We don’t use the method queue pattern yet, but that should come along soon. Stoyan’s post tells us that the Facebook Like button also uses the FIF technique. Meebo is now part of Google, so there’s a good chance that Google Analytics will go this way as well. Here’s hoping that other third party providers do so as well.
onloadevent will only actually fire when the function that creates it has completed and control has returned to the event loop. In our implementation, we execute the
closewithin the same function. This code executes before the iframe’s
onloadevent can fire. When the
onloadevent does fire, our handler has already been registered.
Updated 2013-02-12 to mention issues with
Updated 2013-02-21 we now have a feature request open for the
W3C to add
nonblocking scripts to the HTML5 spec.