Loading Scripts Without Blocking
This post is based on a chapter from Even Faster Web Sites, the follow-up to High Performance Web Sites. Posts in this series include: chapters and contributing authors, Splitting the Initial Payload, Loading Scripts Without Blocking, Coupling Asynchronous Scripts, Positioning Inline Scripts, Sharding Dominant Domains, Flushing the Document Early, Using Iframes Sparingly, and Simplifying CSS Selectors.
As more and more sites evolve into “Web 2.0” apps, the amount of JavaScript increases. This is a performance concern because scripts have a negative impact on page performance. Mainstream browsers (i.e., IE 6 and 7)Â block in two ways:
- Resources in the page are blocked from downloading if they are below the script.
- Elements are blocked from rendering if they are below the script.
The Scripts Block Downloads example demonstrates this. It contains two external scripts followed by an image, a stylesheet, and an iframe. The HTTP waterfall chart from loading this example in IE7 shows that the first script blocks all downloads, then the second script blocks all downloads, and finally the image, stylesheet, and iframe all download in parallel. Watching the page render, you’ll notice that the paragraph of text above the script renders immediately. However, the rest of the text in the HTML document is blocked from rendering until all the scripts are done loading.

Scripts block downloads in IE6&7, Firefox 2&3.0, Safari 3, Chrome 1, and Opera
Browsers are single threaded, so it’s understandable that while a script is executing the browser is unable to start other downloads. But there’s no reason that while the script is downloading the browser can’t start downloading other resources. And that’s exactly what newer browsers, including Internet Explorer 8, Safari 4, and Chrome 2, have done. The HTTP waterfall chart for the Scripts Block Downloads example in IE8 shows the scripts do indeed download in parallel, and the stylesheet is included in that parallel download. But the image and iframe are still blocked. Safari 4 and Chrome 2 behave in a similar way. Parallel downloading improves, but is still not as much as it could be.

Scripts still block, even in IE8, Safari 4, and Chrome 2
Fortunately, there are ways to get scripts to download without blocking any other resources in the page, even in older browsers. Unfortunately, it’s up to the web developer to do the heavy lifting.
There are six main techniques for downloading scripts without blocking:
- XHR Eval – Download the script via XHR and
eval()the responseText. - XHR Injection – Download the script via XHR and inject it into the page by creating a script element and setting its
textproperty to the responseText. - Script in Iframe – Wrap your script in an HTML page and download it as an iframe.
- Script DOM Element – Create a script element and set its
srcproperty to the script’s URL. - Script Defer – Add the script tag’s
deferattribute. This used to only work in IE, but is now in Firefox 3.1. document.writeScript Tag – Write the<script src="">HTML into the page usingdocument.write. This only loads script without blocking in IE.
You can see an example of each technique using Cuzillion. It turns out that these techniques have several important differences, as shown in the following table. Most of them provide parallel downloads, although Script Defer and document.write Script Tag are mixed. Some of the techniques can’t be used on cross-site scripts, and some require slight modifications to your existing scripts to get them to work. An area of differentiation that’s not widely discussed is whether the technique triggers the browser’s busy indicators (status bar, progress bar, tab icon, and cursor). If you’re loading multiple scripts that depend on each other, you’ll need a technique that preserves execution order.
| Technique | Parallel Downloads | Domains can Differ | Existing Scripts | Busy Indicators | Ensures Order | Size (bytes) |
|---|---|---|---|---|---|---|
| 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 | 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 | yes | yes | IE, FF, Saf, Chr, Op | IE, FF, Saf, Chr, Op | ~100 |
The question is: Which is the best technique? The optimal technique depends on your situation. This decision tree should be used as a guide. It’s not as complex as it looks. Only three variables determine the outcome: is the script on the same domain as the main page, is it necessary to preserve execution order, and should the busy indicators be triggered.
Ideally, the logic in this decision tree would be encapsulated in popular HTML templating languages (PHP, Python, Perl, etc.) so that the web developer could just call a function and be assured that their script gets loaded using the optimal technique.
In many situations, the Script DOM Element is a good choice. It works in all browsers, doesn’t have any cross-site scripting restrictions, is fairly simple to implement, and is well understood. The one catch is that it doesn’t preserve execution order across all browsers. If you have multiple scripts that depend on each other, you’ll need to concatenate them or use a different technique. If you have an inline script that depends on the external script, you’ll need to synchronize them. I call this “coupling” and present several ways to do this in Coupling Asynchronous Scripts.
Even Faster Web Sites
This post introduces Even Faster Web Sites, the follow-up to High Performance Web Sites. Posts in this series include: chapters and contributing authors, Splitting the Initial Payload, Loading Scripts Without Blocking, Coupling Asynchronous Scripts, Positioning Inline Scripts, Sharding Dominant Domains, Flushing the Document Early, Using Iframes Sparingly, and Simplifying CSS Selectors.
Last April, I blogged about starting a follow-up to my first book, High Performance Web Sites. Last week I sent in the first round of final edits. Although there will likely be one or two more rounds of edits, they should be small. So, I’m feeling pretty much done. It’s a huge weight off my shoulders. I’ve been working on this book for more than a year. The performance best practices I present required more research than HPWS. I also expanded my testing from just IE and Firefox (as I did in HPWS) to IE, Firefox, Safari, Chrome, and Opera (including multiple versions of each).
The title of this new book is Even Faster Web Sites. It will be published in June, and is available for pre-order now on Amazon and O’Reilly. The cover of HPWS was a greyhound. EFWS’ cover is the Blackbuck Antelope – it can hit 50 mph which is in the top 5 for land animals. (Fastest is cheetah, but that’s taken by Programming the Perl DBI.)
The most exciting thing about EFWS is that it includes six chapters from contributing authors. This came about because I wanted to have best practices for JavaScript performance. I’m a pretty good JavaScript programmer, but not nearly as good as the JavaScript luminaries out there who are writing books and teaching workshops. I also wanted a chapter on image optimization, where Stoyan Stefanov and Nicole Sullivan are the experts. I reached out to folks in these and other areas to contribute performance best practices that they had accumulated. The resulting chapters are listed below. I’ve indicated the contributing authors where appropriate; otherwise, the chapter is written by me.
- Understanding Ajax Performance – Doug Crockford
- Creating Responsive Web Applications – Ben Galbraith and Dion Almaer
- Splitting the Initial Payload
- Loading Scripts Without Blocking
- Coupling Asynchronous Scripts
- Positioning Inline Scripts
- Writing Efficient JavaScript – Nicholas C. Zakas
- Scaling with Comet – Dylan Schiemann
- Going Beyond Gzipping – Tony Gentilcore
- Optimizing Images – Stoyan Stefanov and Nicole Sullivan
- Sharding Dominant Domains
- Flushing the Document Early
- Using Iframes Sparingly
- Simplifying CSS Selectors
- Performance Tools
Between now and when the book comes out, I’ll write a blog post about each of my chapters. I wrote the first one of these, Split the Initial Payload, back in May. Now that I have more time on my hands, I’ll catch up and finish the rest.
If you’re just beginning the process of improving your web site’s performance, you should start with High Performance Web Sites. But as Web 2.0 gains wider adoption and the amount of content on web pages continues to grow, the best practices in Even Faster Web Sites are key to making today’s web sites fast(er).
don’t use @import
In Chapter 5 of High Performance Web Sites, I briefly mention that @import has a negative impact on web page performance. I dug into this deeper for my talk at Web 2.0 Expo, creating several test pages and HTTP waterfall charts, all shown below. The bottomline is: use LINK instead of @import if you want stylesheets to download in parallel resulting in a faster page.
LINK vs. @import
There are two ways to include a stylesheet in your web page. You can use the LINK tag:
<link rel='stylesheet' href='a.css'>
Or you can use the @import rule:
<style>
@import url('a.css');
</style>
I prefer using LINK for simplicity—you have to remember to put @import at the top of the style block or else it won’t work. It turns out that avoiding @import is better for performance, too.
@import @import
I’m going to walk through the different ways LINK and @import can be used. In these examples, there are two stylesheets: a.css and b.css. Each stylesheet is configured to take two seconds to download to make it easier to see the performance impact. The first example uses @import to pull in these two stylesheets. In this example, called @import @import, the HTML document contains the following style block:
<style>
@import url('a.css');
@import url('b.css');
</style>
If you always use @import in this way, there are no performance problems, although we’ll see below it could result in JavaScript errors due to race conditions. The two stylesheets are downloaded in parallel, as shown in Figure 1. (The first tiny request is the HTML document.) The problems arise when @import is embedded in other stylesheets or is used in combination with LINK.
Figure 1. always using @import is okay
LINK @import
The LINK @import example uses LINK for a.css, and @import for b.css:
<link rel='stylesheet' type='text/css' href='a.css'>
<style>
@import url('b.css');
</style>
In IE (tested on 6, 7, and 8), this causes the stylesheets to be downloaded sequentially, as shown in Figure 2. Downloading resources in parallel is key to a faster page. As shown here, this behavior in IE causes the page to take a longer time to finish.
Figure 2. link mixed with @import breaks parallel downloads in IE
LINK with @import
In the LINK with @import example, a.css is inserted using LINK, and a.css has an @import rule to pull in b.css:
<link rel='stylesheet' type='text/css' href='a.css'>
@import url('b.css');
This pattern also prevents the stylesheets from loading in parallel, but this time it happens on all browsers. When we stop and think about it, we shouldn’t be too surprised. The browser has to download a.css and parse it. At that point, the browser sees the @import rule and starts to fetch b.css.
Figure 3. using @import from within a LINKed stylesheet breaks parallel downloads in all browsers
LINK blocks @import
A slight variation on the previous example with surprising results in IE: LINK is used for a.css and for a new stylesheet called proxy.css. proxy.css is configured to return immediately; it contains an @import rule for b.css.
<link rel='stylesheet' type='text/css' href='a.css'> <link rel='stylesheet' type='text/css' href='proxy.css'>
@import url('b.css');
The results of this example in IE, LINK blocks @import, are shown in Figure 4. The first request is the HTML document. The second request is a.css (two seconds). The third (tiny) request is proxy.css. The fourth request is b.css (two seconds). Surprisingly, IE won’t start downloading b.css until a.css finishes. In all other browsers, this blocking issue doesn’t occur, resulting in a faster page as shown in Figure 5.

Figure 4. LINK blocks @import embedded in other stylesheets in IE

Figure 5. LINK doesn't block @import embedded stylesheets in browsers other than IE
many @imports
The many @imports example shows that using @import in IE causes resources to be downloaded in a different order than specified. This example has six stylesheets (each takes two seconds to download) followed by a script (a four second download).
<style>
@import url('a.css');
@import url('b.css');
@import url('c.css');
@import url('d.css');
@import url('e.css');
@import url('f.css');
</style>
<script src='one.js' type='text/javascript'></script>
Looking at Figure 6, the longest bar is the four second script. Even though it was listed last, it gets downloaded first in IE. If the script contains code that depends on the styles applied from the stylesheets (a la getElementsByClassName, etc.), then unexpected results may occur because the script is loaded before the stylesheets, despite the developer listing it last.

Figure 6. @import causes resources to be downloaded out-of-order in IE
LINK LINK
It’s simpler and safer to use LINK to pull in stylesheets:
<link rel='stylesheet' type='text/css' href='a.css'> <link rel='stylesheet' type='text/css' href='b.css'>
Using LINK ensures that stylesheets will be downloaded in parallel across all browsers. The LINK LINK example demonstrates this, as shown in Figure 7. Using LINK also guarantees resources are downloaded in the order specified by the developer.
Figure 7. using link ensures parallel downloads across all browsers
These issues need to be addressed in IE. It’s especially bad that resources can end up getting downloaded in a different order. All browsers should implement a small lookahead when downloading stylesheets to extract any @import rules and start those downloads immediately. Until browsers make these changes, I recommend avoiding @import and instead using LINK for inserting stylesheets.
Update: April 10, 2009 1:07 PM
Based on questions from the comments, I added two more tests: LINK with @imports and Many LINKs. Each of these insert four stylesheets into the HTML document. LINK with @imports uses LINK to load proxy.css; proxy.css then uses @import to load the four stylesheets. Many LINKs has four LINK tags in the HTML document to pull in the four stylesheets (my recommended approach). The HTTP waterfall charts are shown in Figure 8 and Figure 9.

Figure 8. LINK with @imports

Figure 9. Many LINKs
Looking at LINK with @imports, the first problem is that the four stylesheets don’t start downloading until after proxy.css returns. This happens in all browsers. On the other hand, Many LINKs starts downloading the stylesheets immediately.
The second problem is that IE changes the download order. I added a 10 second script (the really long bar) at the very bottom of the page. In all other browsers, the @import stylesheets (from proxy.css) get downloaded first, and the script is last, exactly the order specified. In IE, however, the script gets inserted before the @import stylesheets, as shown by LINK with @imports in Figure 8. This causes the stylesheets to take longer to download since the long script is using up one of only two connections available in IE 6&7. Since IE won’t render anything in the page until all stylesheets are downloaded, using @import in this way causes the page to be blank for 12 seconds. Using LINK instead of @import preserves the load order, as shown by Many LINKs in Figure 9. Thus, the page renders in 4 seconds.
The load times of these resources are exaggerated to make it easy to see what’s happening. But for people with slow connections, especially those in some of the world’s emerging markets, these response times may not be that far from reality. The takeaways are:
- Using @import within a stylesheet adds one more roundtrip to the overall download time of the page.
- Using @import in IE causes the download order to be altered. This may cause stylesheets to take longer to download, which hinders progress rendering making the page feel slower.

