Managed XHR Injection
Update (2009 Dec 2 @ 12:09PM): I started to question if Managed XHR Injection was all unnecessary fluff or if I could just write script tags to the page. Then I reread parts of Chapter 4 and realized that Managed XHR Injection parallelizes downloads and ensures execution order in all browsers, something that neither the Script DOM Element nor document.write Script Tag approach offers. (Script DOM Element parallelizes downloads in all browsers but only ensures execution order in Firefox and Opera. document.write Script Tag ensures order in all browsers but does not parallelize downloads in Firefox.)
Steve Souders recently released a follow-up to High-Performance Web Sites called Even Faster Web Sites.
In Chapter 3, Splitting the Intial Payload, he describes the virtues of splitting your JavaScript into that which must be downloaded as the page is loaded and that which can be downloaded later. The reason being that typical browser behavior dictates that when a script tag is encountered that references an external script, no additional downloads (for scripts, images, or anything else) are allowed until the external script has been downloaded, parsed, and executed. Which means that all explicit script tags in your HTML referencing external scripts constitute a blocking operation.
The drastic example he gives is of Facebook’s homepage, which loads 14 scripts totaling 786 kilobytes. The catch is that only 9% of the functions declared in those 14 scripts are actually called by the page before the onload event is raised, meaning that 91% of the functions are not necessary for the loading and rendering of the page and therefore don’t need to be downloaded in a blocking manner.
In Chapter 4, Loading Scripts Without Blocking, he describes several methods for downloading external scripts after onload in a non-blocking manner. It is important that referenced scripts are executed in the order they are referenced, but the order in which they are downloaded is unimportant. This allows the developer to download scripts in parallel. One method described is XHR Injection, in which a script is downloaded via AJAX, then appended to a new script tag, which is then appended to the document to execute it. The code to do so is trivial, but order of execution is not guaranteed. He goes on to describe Managed XHR Injection, in which downloads are queued in parallel but execution is done in sequence once all scripts are downloaded. He does not provide any code for such a solution so I decided to give it a try:
$.downloadScripts = function(urls)
{
// Map the URLs to objects containing the URLs and default flags.
var scripts = $.map
(
urls,
function(url)
{
return { url: url, downloadAttempted: false, injectable: false, data: undefined };
}
);
$.each
(
scripts,
function(n, script)
{
// Attempt to download script.
$.ajax
(
{
url: script.url,
// Need to know when errors occur so that these scripts aren't waited on for execution to start.
error: function()
{
script.downloadAttempted = true;
},
success: function(data, status)
{
// Save the contents for later and mark it as downloaded.
script.data = data;
script.injectable = script.downloadAttempted = true;
var allDownloadsAttempted = true;
for (var n = 0; n < scripts.length && allDownloadsAttempted; n++)
allDownloadsAttempted &= scripts[n].downloadAttempted;
// If no more scripts are being waited on...
if (allDownloadsAttempted)
{
// Loop over scripts, in original order, and append a script tag
// to the body for those that are injectable.
for (var n = 0; n < scripts.length; n++)
{
if (scripts[n].injectable)
$("body").append("<script>" + scripts[n].data + "</script>");
}
delete scripts;
}
}
}
);
}
);
};
It is implemented as a jQuery plugin, which means you’ll need three explicit script tags in your HTML for:
- Referencing the jQuery library.
- Referencing the file containing downloadScripts.
- A call to downloadScripts to download scripts after onload.
So:
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
<script type="text/javascript" src="ManagedXhrInjection.min.js"></script>
<script type="text/javascript">
$(document).ready
(
function()
{
$.downloadScripts(["1.js", "2.js", "3.js"]);
}
);
</script>
It works by mapping the passed array of script URLs to a new array of objects containing each URL and flags to control execution. It handles the case of a bad script URL by specifying that the download was attempted. When a script is successfully downloaded, it checks to see if there are any more scripts to attempt to download and if not, it creates and appends new script elements for those scripts that were successfully downloaded.
A 362 byte obfuscated and minified version:
$.downloadScripts=function(a){var b=$.map(a,function(c){return{d:c,e:false,f:false,g:undefined}});$.each(b,function(h,i){$.ajax({url:i.d,error:function(){i.e=true;},success:function(j){i.g=j;i.f=i.e=true;var k=true;for(var l=0;l<b.length&&k;l++)k&=b[l].e;if(k){for(var m=0;m<b.length;m++)if(b[m].f)$("body").append("<script>"+b[m].g+"</script>");delete b}}})})};