Chapter 4. Loading Scripts Without Blocking

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.

Scripts Block

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]

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.

Scripts block parallel downloads

Figure 4-1. Scripts block parallel downloads

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.

Internet Explorer 8 downloads scripts without blocking

Figure 4-2. Internet Explorer 8 downloads scripts without blocking

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.

Making Scripts Play Nice

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.

XHR Eval

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.)

Loading scripts using XHR Eval

Figure 4-3. Loading scripts using XHR Eval

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;
    }
}

XHR Injection

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('');

Script in Iframe

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.

Script DOM Element

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 Defer

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.

document.write Script Tag

This last technique uses document.write to put the SCRIPT HTML tag into the page.

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>");

Browser Busy Indicators

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.

Busy indicators in the browser

Figure 4-4. Busy indicators in the browser

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

document.write Script Tag[b]

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 document.write Script Tag achieves parallel downloads only in Internet Explorer, Safari 4, and Chrome 2.

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.

Ensuring (or Avoiding) Ordered Execution

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.

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.

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.

Summarizing the Results

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

document.write Script Tag

(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.

And the Winner Is

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.

Decision tree for selecting script loading technique

Figure 4-5. Decision tree for selecting script loading technique

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.