Optimizing External Javascript: Be Lazy, And Efficient
The Risks And Reality of Third-party Javascript
Ever had your site "broken" due to loading Javascript from a remote site that went down, effectively taking yours with it? Using third-party (remote) script references, security concerns aside, introduces a reliability risk and can effectively prevent your site from rendering, which happens more often than necessary - but is avoidable.
With the current trend of web services springing up, there is no shortage of remote services driven by Javascript which you may want to add to your site via <script>
. Web analytics, keyword and traffic services, commenting systems, bookmarking and JSON-formatted media feeds are a few common examples.
How <script>
Can Break Your Site
Traditionally, a <script>
node blocks (or "hangs") the browser while it is being processed, preventing the rest of the page from loading. Because document.write()
or other javascript in the block may inject HTML into the document structure at parse time, browsers generally have to stop rendering in order to parse and evaluate the javascript block before continuing. This is undesirable.
Where this becomes particularly troublesome is when including third-party javascript, eg., a remote analytics service. If this resource were slow to load, became unavailable or went offline, your site would be frozen while the browser hangs on an unresponsive or broken connection. Numerous large sites including TechCrunch have been hit by, and reported on issues arising from third-party scripts. Unnecessary "downtime" due to a third party bit of Javascript is an embarrassing annoyance which, if tied to blocking issues, can be avoided.
Avoiding Site Hang-Ups
Sometimes, it's good to be lazy for the sake of performance. There are a few ways to work around the blocking nature of script elements which load slowly, or perhaps don't load at all, from both local and third-party sites.
The defer
Attribute
Despite being criticized for their IE-only features, Microsoft have come up with a number of good ones over the years (eg. xmlHttpRequest, among others.) It appears they also thought of the script blocking issue, as well.
Internet Explorer has for years supported the defer
attribute on <script>
nodes, (reference) which instructs the browser to defer parse and execution of the script until a later time. This does not mean that script loading is deferred, so blocking of rendering may still occur even if the browser's "downloading and parsing" process is not interrupted.
<script type="text/javascript" src="foo.js" defer="defer"></script>
Deferring and document.write()
calls are at odds with each other, so be careful when deferring scripts that use it.
Though not universally-supported yet, Firefox 3.1 is reported to be introducing support for defer
, as Opera does. (Source) Opera 9.5 also has a "deferred script loading" user preference, disabled by default, which reportedly can defer and supports document.write()
calls as well. (Source)
Performance gains can also be made by deferring script, as that the browser can focus on downloading content and doing layout before having to parse and execute potentially-expensive Javascript code which further modifies the DOM.
Load Javascript Using.. Javascript
Once the document has been parsed (DOMContentReady etc.) or within an inline script block, one may safely create and append <script>
elements to the DOM dynamically via Javascript. Because the document has parsed and rendered, script requests no longer block rendering and their load, parse and execution are effectively deferred. This is safe provided that the code you're loading does not include use of document.write()
, which will "blank" the page if called after parsing has completed.
Dynamically-loading Javascript example
Creating a script node, assigning the .src attribute and appending it to the document head. The script should be downloaded, parsed and executed immediately, as though it were referenced statically from HTML.
function loadScript(sURL) {
var oS = document.createElement('script');
oS.type = 'text/javascript';
oS.src = sURL;
document.getElementsByTagName('head')[0].appendChild(oS);
};
Dynamically-loading Javascript (with readyState handler)
Creating and appending a script node including an optional onload()
-style event callback for when the script has loaded. This is useful in the case where you need to load a script which does not have a callback of its own (eg., code which defines a data structure but does not pass it to a predefined function.)
function loadScript(sURL,fOnLoad) {
function scriptOnload() {
this.onreadystatechange = null;
this.onload = null;
window.setTimeout(fOnLoad,20);
};
var loadScriptHandler = function() {
var rs = this.readyState;
if (rs == 'loaded' || rs == 'complete') {
scriptOnload();
}
};
var oS = document.createElement('script');
oS.type = 'text/javascript';
if (fOnLoad) {
// hook into both possible events
oS.onreadystatechange = loadScriptHandler;
oS.onload = scriptOnload;
};
oS.src = sURL;
document.getElementsByTagName('head')[0].appendChild(oS);
};
Cache Static Content Locally
This is self-explanatory, but is worth mentioning. If it's static content, why pull it from a remote source? It really should be served (and if possible, deferred) locally.
Is It Worth Doing?
Though defer
and dynamic loading both make script behave asynchronously, the approach should be quite familiar to modern developers used to working with Ajax-style applications. Ultimately, this method trades a change in programming style for a more failure-proof method which can also improve initial page render and perceived performance.