SCRIPT
tags have a negative
impact on page performance because of their blocking behavior.
While scripts are being downloaded and executed, most browsers won’t
download anything else. There are times when it’s necessary to have this
blocking, but it’s important to identify situations when JavaScript can be
loaded independent of the rest of the page.
When these opportunities arise, we want to load the JavaScript in such a way that it does not block other downloads. Luckily, there are several techniques for doing this that make pages load faster. This chapter explains these techniques, compares how they affect the browser and performance, and describes the circumstances that make one approach preferred over another.
JavaScript is included in a web page as an inline script or an external script. An
inline script includes all the JavaScript in the HTML document itself using the SCRIPT
tag:
<script> function displayMessage(msg) { alert(msg); } </script>
External scripts pull in the JavaScript from a separate file using
the SCRIPT SRC
attribute:
<script src='A.js'></script>
The SRC
attribute specifies the
URL of the external file that needs to be loaded. The browser reads the
script file from the cache, if available, or makes an HTTP request to
fetch the script.
Normally, most browsers download components in parallel, but that’s not the case for external scripts. When the browser starts downloading an external script, it won’t start any additional downloads until the script has been completely downloaded, parsed, and executed. (Any downloads that were already in progress are not blocked.)
Figure 4-1 shows the HTTP requests for the Scripts Block Downloads example.[9]
- Scripts Block Downloads
http://stevesouders.com/cuzillion/?ex=10008&title=Scripts+Block+Downloads
This page has two scripts at the top, A.js and B.js, followed by an image, a stylesheet, and an iframe. The scripts are each programmed to take one second to download and one second to execute. The white gaps in the HTTP profile indicate where the scripts are executed. This shows that while scripts are being downloaded and executed, all other downloads are blocked. Only after the scripts have finished are the image, stylesheet, and iframe merrily downloaded in parallel.
The reason browsers block while downloading and executing a script
is that the script may make changes to the page or JavaScript namespace
that affect whatever follows. The typical example cited is when A.js uses document.write
to alter the page. Another
example is when A.js is a
prerequisite for B.js. The developer
is guaranteed that scripts are executed in the order in which
they appear in the HTML document so that A.js is downloaded and executed before
B.js. Without this guarantee, race
conditions could result in JavaScript errors if B.js is downloaded and executed before
A.js.
Although it’s clear that scripts must be executed sequentially, there’s no reason they have to be downloaded sequentially. That’s where Internet Explorer 8 comes in. The behavior shown in Figure 4-1 is true for most browsers, including Firefox 3.0 and earlier and Internet Explorer 7 and earlier. However, Internet Explorer 8’s download profile, shown in Figure 4-2, is different. Internet Explorer 8 is the first browser that supports downloading scripts in parallel.
The ability of Internet Explorer 8 to download scripts in parallel makes pages load faster, but as shown in Figure 4-2, it doesn’t entirely solve the blocking problem. It is true that A.js and B.js are downloaded in parallel, but the image and iframe are still blocked until the scripts are downloaded and executed. Safari 4 and Chrome 2 are similar—they download scripts in parallel, but block other resources that follow.[10]
What we really want is to have scripts downloaded in parallel with all the other components in the page. And we want this in all browsers. The techniques discussed in the next section explain how to do just that.
There are several techniques for downloading external scripts without having your page suffer from their blocking behavior. One technique I don’t suggest doing is inlining all of your JavaScript. In a few situations (home pages, small amounts of JavaScript), inlining your JavaScript is acceptable, but generally it’s better to serve your JavaScript in external files because of the page size and caching benefits derived. (For more information about these trade-offs, see High Performance Web Sites, “Rule 8: Make JavaScript and CSS External.”)
The techniques listed here provide the benefits of external scripts without the slowdowns imposed from blocking:
XHR Eval
XHR Injection
Script in Iframe
Script DOM Element
Script Defer
document.write
Script Tag
The following sections describe each of these techniques in more detail, followed by a comparison of how they affect the browser and which technique is best under different circumstances.
In this technique, an XMLHttpRequest
(XHR) retrieves the JavaScript
from the server. When the response is complete, the content is executed
using the eval
command as shown
in this example page.
As you can see in the HTTP profile in Figure 4-3, the XMLHttpRequest
doesn’t block the other
components in the page—all five resources are downloaded in parallel.
The scripts are executed after they finish downloading. (This execution
time doesn’t show up on the HTTP waterfall chart because no network
activity is involved.)
The main drawback of this approach is that the XMLHttpRequest
must be served from the same
domain as the main page. The relevant source code from the XHR Eval
example follows:[11]
var xhrObj = getXHRObject(); xhrObj.onreadystatechange = function() { if ( xhrObj.readyState == 4 && 200 == xhrObj.status ) { eval(xhrObj.responseText); } }; xhrObj.open('GET', 'A.js', true); // must be same domain as main page xhrObj.send(''); function getXHRObject() { var xhrObj = false; try { xhrObj = new XMLHttpRequest(); } catch(e){ var progid = ['MSXML2.XMLHTTP.5.0', 'MSXML2.XMLHTTP.4.0', 'MSXML2.XMLHTTP.3.0', 'MSXML2.XMLHTTP', 'Microsoft.XMLHTTP']; for ( var i=0; i < progid.length; ++i ) { try { xhrObj = new ActiveXObject(progid[i]); } catch(e) { continue; } break; } } finally { return xhrObj; } }
Like XHR Eval, the XHR Injection technique uses an XMLHttpRequest
to retrieve the JavaScript. But
instead of using eval
, the JavaScript
is executed by creating a script DOM element and injecting the XMLHttpRequest
response into the script. Using
eval
is potentially slower than using
this mechanism.
The XMLHttpRequest
must be
served from the same domain as the main page. The relevant source code
from the XHR Injection example follows:
var xhrObj = getXHRObject(); // defined in the previous example xhrObj.onreadystatechange = function() { if ( xhrObj.readyState == 4 ) { var scriptElem = document.createElement('script'); document.getElementsByTagName('head')[0].appendChild(scriptElem); scriptElem.text = xhrObj.responseText; } }; xhrObj.open('GET', 'A.js', true); // must be same domain as main page xhrObj.send('');
Iframes are loaded in parallel with other components in the main page. Whereas iframes are typically used to include one HTML page within another, the Script in Iframe technique leverages them to load JavaScript without blocking, as shown by the Script in Iframe example.
The implementation is done entirely in HTML:
<iframe src='A.html' width=0 height=0 frameborder=0 id=frame1></iframe>
Note that this technique uses A.html instead of A.js. This is necessary because the iframe expects an HTML document to be returned. All that is needed is to convert the external script to an inline script within an HTML document.
Similar to the XHR Eval and XHR Injection approaches, this
technique requires that the iframe URL be served from the same domain as
the main page. (Browser cross-site security restrictions prevent
JavaScript access from an iframe to a cross-domain parent and vice
versa.) Even when the main page and iframe are served from the same
domain, it’s still necessary to modify your JavaScript to create a
connection between them. One approach is to have the parent reference
JavaScript symbols in the iframe via the frames
array or document.getElementById
:
// access the iframe from the main page using "frames" window.frames[0].createNewDiv(); // access the iframe from the main page using "getElementById" document.getElementById('frame1').contentWindow.createNewDiv();
The iframe references its parent using the parent
variable:
// access the main page from within the iframe using "parent" function createNewDiv() { var newDiv = parent.document.createElement('div'); parent.document.body.appendChild(newDiv); }
Iframes also have an innate cost. In fact, they’re the most expensive DOM element by at least an order of magnitude, as discussed in Chapter 13.
Rather than using the SCRIPT
tag in
HTML to download a script file, this technique uses JavaScript to create
a script DOM element and set the SRC
property dynamically. This can be done
with just a few lines of JavaScript:
var scriptElem = document.createElement('script'); scriptElem.src = 'http://anydomain.com/A.js'; document.getElementsByTagName('head')[0].appendChild(scriptElem);
Creating a script this way does not block other components during download. As opposed to the previous techniques, Script DOM Element allows you to fetch the JavaScript from a server other than the one used to fetch the main page. The code to implement this technique is short and simple. Your external script file can be used as is and doesn’t need to be refactored as in the XHR Eval and Script in Iframe approaches.
- Script DOM Element
http://stevesouders.com/cuzillion/?ex=10010&title=Script+Dom+Element
Internet Explorer supports the SCRIPT DEFER
attribute as a way for developers to tell the browser that the script
does not need to be loaded immediately. This is a safe attribute to use
when a script does not contain calls to document.write
and no other scripts in the
page depend on it. When Internet Explorer downloads the deferred script,
it allows other downloads to be done in parallel.
The DEFER
attribute is an easy
way to avoid the bad blocking behavior of scripts with the addition of a
single word:
<script defer src='A.js'></script>
Although DEFER
is part
of the HTML 4 specification, it is implemented only in Internet
Explorer and in some newer browsers.
This last technique uses document.write
to put
the SCRIPT
HTML tag into the
page.
document.write
Script Taghttp://stevesouders.com/cuzillion/?ex=10014&title=document.write+Script+Tag
This technique, similar to Script Defer, results in parallel
script loading in Internet Explorer only. Although it allows multiple
scripts to be downloaded in parallel (provided all the document.write
lines occur in the same script
block), other types of resources
remain blocked while scripts are downloading:
document.write("<script type='text/javascript' src='A.js'><\/script>");
All of the techniques described in the preceding section improve how JavaScript is downloaded by allowing multiple resources to be downloaded in parallel. But these techniques differ in certain other aspects. One area of differentiation is how they affect the user’s perception of whether the page is loaded. Browsers offer multiple browser busy indicators that give the user clues that the page is still loading.
Figure 4-4 shows four browser busy indicators: the status bar, the progress bar, the tab icon, and the cursor. The status bar shows the URL of the current download. The progress bar moves across the bottom of the window as downloads complete. The logo spins while downloads are happening. The cursor changes to an hourglass or similar cursor to indicate that the page is busy.
The other two browser busy indicators are blocked rendering and
blocked onload
event. Blocked
rendering is very obtrusive to the user experience. When scripts are being
downloaded in the typical manner using SCRIPT SRC
, nothing
below the script is rendered.
Freezing the page before it’s fully rendered is a severe way of showing
the browser is busy.
Typically, the page’s onload
event doesn’t fire until all resources have been downloaded. This may
affect the user experience if the status bar takes longer to say “Done”
and setting focus on the default input field is delayed.
Whereas most of these browser busy indicators are triggered when
downloading JavaScript in the usual SCRIPT
SRC
way, none of them are triggered by the XHR Eval and XHR
Injection techniques when using Internet Explorer, Firefox, and Opera. The busy indicators
that are triggered vary depending on the technique used and the browser
being tested.
Table 4-1 shows which busy indicators occur for each of the JavaScript download techniques. XHR Eval and XHR Injection trigger the fewest busy indicators. The other techniques have mixed behavior. Although busy indicators vary across browsers, they’re generally consistent across different browser versions.
Table 4-1. Browser busy indicators triggered by JavaScript downloads
Technique | Status bar | Progress bar | Logo | Cursor | Block render | Block onload |
---|---|---|---|---|---|---|
Normal Script Src | FF, Saf, Chr | IE, FF, Saf | IE, FF, Saf, Chr | FF, Chr | IE, FF, Saf, Chr, Op | IE, FF, Saf, Chr, Op |
XHR Eval | Saf, Chr | Saf | Saf, Chr | Saf, Chr | -- | -- |
XHR Injection | Saf, Chr | Saf | Saf, Chr | Saf, Chr | -- | -- |
Script in Iframe | IE, FF, Saf, Chr | FF, Saf | IE, FF, Saf, Chr | FF, Chr | -- | IE, FF, Saf, Chr, Op |
Script DOM Element | FF, Saf, Chr | FF, Saf | FF, Saf, Chr | FF, Chr | -- | FF, Saf, Chr |
Script Defer[a] | FF, Saf, Chr | FF, Saf | FF, Saf, Chr | FF, Chr, Op | FF, Saf, Chr, Op | IE, FF, Saf, Chr, Op |
| FF, Saf, Chr | IE, FF, Saf | IE, FF, Saf, Chr | FF, Chr, Op | IE, FF, Saf, Chr, Op | IE, FF, Saf, Chr, Op |
[a] Script Defer achieves parallel downloads in Firefox 3.1 and later. [b] Note that |
Note
Abbreviations are as follows: (Chr) Chrome 1.0.154 and 2.0.156; (FF) Firefox 2.0, 3.0, and 3.1; (IE) Internet Explorer 6, 7, and 8; (Op) Opera 9.63 and 10.00 alpha; (Saf) Safari 3.2.1 and 4.0 (developer preview).
It’s important to understand how each technique behaves with regard to the browser busy indicators. In some cases, the busy indicators are desirable for a better user experience: they let the user know the page is working. In other situations, it would be better not to show any busy activity, thus encouraging users to start interacting with the page.
In many cases, a web page contains multiple scripts that have a particular
dependency order. Using the normal SCRIPT
SRC
approach guarantees that the scripts are downloaded and executed in the order in which
they are listed in the page. However, using certain of the advanced
downloading techniques described previously does not carry such a
guarantee. Because the scripts are downloaded in parallel, they may get
executed in the order in which they arrive—the fastest response to arrive
being executed first—rather than the order in which they were listed. This
can lead to race conditions resulting in undefined symbol errors.
Some of the techniques do ensure ordered execution, but they vary
depending on the browser. For Internet Explorer, the Script Defer and document.write
Script Tag approaches that guarantee scripts are
executed in the order listed, regardless of which is downloaded first. For
instance, the IE Ensure Ordered Execution example contains three scripts
that are loaded using Script Defer. Even though the first script (with
sleep=3
in the URL) finishes
downloading last, it is still the first to be executed.
- IE Ensure Ordered Execution
http://stevesouders.com/cuzillion/?ex=10017&title=IE+Ensure+Ordered+Execution
Because the Script Defer and document.write
Script Tag techniques don’t
achieve parallel script downloads in Firefox, you need to use a different technique whenever one
script depends on another. The Script DOM Element approach guarantees that
scripts are executed in the order listed in Firefox. The FF Ensure Ordered
Execution example contains three scripts that are loaded using the Script
DOM Element approach. Even though the first script (with sleep=3
in the URL) finishes downloading last,
it is still the first to be executed.
- FF Ensure Ordered Execution
http://stevesouders.com/cuzillion/?ex=10018&title=FF+Ensure+Ordered+Execution
It’s not always important to ensure that scripts are executed in the order specified. Sometimes you actually want the browser to execute whatever script happens to come first, because that produces a page that loads faster. One example is a web page containing multiple widgets (A, B, and C) with associated scripts (A.js, B.js, and C.js) that do not have any interdependencies. Even though the page might list the widget scripts in that order, a better user experience would result from executing whichever widget script is received first. The XHR Eval and XHR Injection techniques achieve this. The Avoid Ordered Execution example executes the first script downloaded, even though it’s not the first script listed in the page.
- Avoid Ordered Execution
http://stevesouders.com/cuzillion/?ex=10019&title=Avoid+Ordered+Execution
I’ve presented several advanced techniques for downloading external scripts and various trade-offs between them. Table 4-2 summarizes the results.
Table 4-2. Summary of advanced script downloading techniques
Technique | Parallel downloads | Domains can differ | Existing scripts | Busy indicators | Ensures order | Size (bytes) |
---|---|---|---|---|---|---|
Normal Script Src | (IE8, Saf4)[a] | Yes | Yes | IE, Saf4, (FF, Chr)[b] | IE, Saf4, (FF, Chr, Op)[c] | ~50 |
XHR Eval | IE, FF, Saf, Chr, Op | No | No | Saf, Chr | -- | ~500 |
XHR Injection | IE, FF, Saf, Chr, Op | No | Yes | Saf, Chr | -- | ~500 |
Script in Iframe | IE, FF, Saf, Chr, Op[d] | No | No | IE, FF, Saf, Chr | -- | ~50 |
Script DOM Element | IE, FF, Saf, Chr, Op | Yes | Yes | FF, Saf, Chr | FF, Op | ~200 |
Script Defer | IE, Saf4, Chr2, FF3.1 | Yes | Yes | IE, FF, Saf, Chr, Op | IE, FF, Saf, Chr, Op | ~50 |
| (IE, Saf4, Chr2, Op)[e] | Yes | Yes | IE, FF, Saf, Chr, Op | IE, FF, Saf, Chr, Op | ~100 |
[a] Scripts are downloaded in parallel with other scripts, but other types of resources are blocked from downloading. [b] These browsers do not, however, support parallel downloads with this technique. [c] See note a above. [d] An interesting performance boost in Opera is that in addition to the script iframes being downloaded in parallel, the code is executed in parallel, too. [e] See note b above. |
Note
Abbreviations are as follows: (Chr) Chrome 1.0.154 and 2.0.156; (FF) Firefox 2.0 and 3.1; (IE) Internet Explorer 6, 7, and 8; (Op) Opera 9.63 and 10.00 alpha; (Saf) Safari 3.2.1 and 4.0 (developer preview).
These techniques allow scripts to be downloaded in parallel with all the other resources in the page, something that browsers don’t do by default, even newer browsers. This can significantly speed up your web page. This is especially important for Web 2.0 applications, where the number and size of external scripts are greater than in other web pages.
The document.write
Script Tag
technique is less preferred because it parallelizes
downloads only in a subset of browsers and blocks parallel downloads for
anything other than script resources. Script Defer also parallelizes downloads in only some
browsers.
XHR Eval, XHR Injection, and Script in Iframe carry the requirement that your scripts reside on the same hostname as the main page. To use the XHR Eval and Script in Iframe techniques, you must refactor your scripts slightly, whereas the XHR Injection and Script DOM Element approaches can download your existing script files without any changes. An estimate of the number of characters added to the page to implement each technique is shown in the “Size” column in Table 4-2.
The different effects that each technique has on the browser’s busy indicators bring in another set of considerations. If you’re downloading scripts that are incidental to the initial rendering of the page (i.e., “lazy-loading”), techniques that make the page appear complete are preferred, such as XHR Eval and XHR Injection. If you want to indicate to the user that the page is still loading while the browser downloads scripts, Script in Iframe is better because it triggers more browser busy indicators.
The final issue of ordered execution favors some techniques over others depending on whether load order matters. If you want scripts to be downloaded in parallel with other resources but executed in a specific order, it’s necessary to mix techniques by browser. If load order doesn’t matter, XHR Eval and XHR Injection can be used.
My conclusion is that there is no single best solution. The preferred approach depends on your requirements. Figure 4-5 shows the decision tree for selecting the best technique for downloading scripts.
There are six possible outcomes in this decision tree:
- Different Domains, No Order
XHR Eval, XHR Injection, and Script in Iframe can’t be used under these conditions because the domain of the main page is different from the domain of the script. Script Defer shouldn’t be used because it forces scripts to be loaded in order, whereas the page loads faster if scripts are executed as soon as they arrive. For this situation, Script DOM Element is the best alternative. In Firefox, load order is preserved even though that’s not desired. Note that both of these techniques trigger the busy indicators, so there’s no way to avoid that. Examples of web pages that match this situation are pages that contain JavaScript-enabled ads and widgets. The scripts for these ads and widgets are likely on domains that differ from the main page, but they don’t have any interdependencies, so load order doesn’t matter.
- Different Domains, Preserve Order
As before, because the domains of the main page and scripts are different, XHR Eval, XHR Injection, and Script in Iframe are not viable alternatives. To ensure load order, Script Defer should be used for Internet Explorer and Script DOM Element for Firefox. Note that both of these techniques trigger the busy indicators. An example of a page that matches these requirements is a page pulling in multiple JavaScript files from different servers that have interdependencies.
- Same Domain, No Order, No Busy Indicators
XHR Eval and XHR Injection are the only techniques that do not trigger the busy indicators. Of the two XHR techniques, I prefer XHR Injection because it can be used without refactoring the existing scripts. This technique would apply to a web page that wanted to download its own JavaScript file in the background, as described in Chapter 3.
- Same Domain, No Order, Show Busy Indicators
XHR Eval, XHR Injection, and Script in Iframe are the only techniques that do not preserve load order across both Internet Explorer and Firefox. Script in Iframe seems to be the best choice because it triggers the busy indicators and increases the size of the page only slightly, but I prefer XHR Injection because it can be used without any refactoring of the existing scripts and it’s already a choice for other decision tree outcomes. Additional client-side JavaScript is required to activate the busy indicators: the status bar and cursor can be activated when the XHR is sent and then deactivated when the XHR returns. I call this “Managed XHR Injection.”
- Same Domain, Preserve Order, No Busy Indicators
XHR Eval and XHR Injection are the only techniques that do not trigger the busy indicators. Of the two XHR techniques, I prefer XHR Injection because it can be used without refactoring the existing scripts. To preserve load order, another type of “Managed XHR Injection” is needed. In this case, the XHR responses are queued if necessary to handle the situation where a script that needs to be loaded later in the order is not executed until all the preceding scripts have been downloaded and executed. An example of a page in this situation is one where multiple interdependent scripts need to be downloaded in the background.
- Same Domain, Preserve Order, Show Busy Indicators
Script Defer for Internet Explorer and Script DOM Element for Firefox are the preferred solutions here. Managed XHR Injection and Managed XHR Eval are other valid alternatives, but they add more code to the main page and are more complicated to implement.
The next step is to implement this logic in code by providing a simple function that developers can use to make sure they load scripts in the optimal way. A prototype for such a function would look like this:
function loadScript(url, bPreserveOrder, bShowBusy);
To avoid downloading more JavaScript than necessary, a backend implementation in a language invoked by the server, such as Perl, PHP, or Python, would be the most useful. In their backend templates, web developers would call this function and the appropriate technique would be inserted into the HTML document response. Providing support for these advanced best practices in development frameworks is the appropriate next step for getting wider adoption.
[9] This and other examples are generated from Cuzillion, a tool I built specifically for this chapter. See the Appendix A for more information about Cuzillion.
[10] As of this writing, Firefox does not yet support parallel script downloads, but that is expected soon.
[11] If you’re using a JavaScript library, it probably has a
wrapper for XMLHttpRequest
, such
as jQuery.ajax
or dojo.xhrGet
. Use that instead of writing
your own wrapper.
Get Even Faster Web Sites 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.