Web Performance Calendar

The speed geek's favorite time of year
2016 Edition
ABOUT THE AUTHOR

Steve works at SpeedCurve on the interplay between performance and design. He previously served as Google's Head Performance Engineer, Chief Performance Yahoo!, and Chief Performance Officer at Fastly. Steve has pioneered much of the work in the world of web performance. He is the author of High Performance Web Sites and Even Faster Web Sites. He is the creator of many performance tools and services including YSlow, the HTTP Archive, Episodes, ControlJS, and Browserscope. Steve taught CS193H: High Performance Web Sites at Stanford and serves as co-chair of Velocity, the web performance and operations conference from O'Reilly.

Every week I analyze a handful of websites for rendering performance. The main culprit blocking rendering is JavaScript execution. Frequently, I notice this blocking JavaScript is loaded with the ASYNC tag. Wha?! Shouldn’t ASYNC not block rendering? Sadly, no. Not only can ASYNC scripts block rendering, they can also block synchronous scripts from executing. Presumably, scripts that are made synchronous are important for the critical content on the page, so delaying their execution causes further harm to the user experience.

In this article, I make the argument that DEFER should be the default choice over ASYNC. To set the stage, I’m going to review how browsers evolved to make scripts faster with preloaders and the motivation for the ASYNC and DEFER attributes. If you know all of that stuff, then skip to the ASYNC and DEFER section.

A few conventions:

  • “script” typically means an external JavaScript file that has to be downloaded
  • “synchronous script” means a script done without ASYNC and DEFER which thus blocks HTML parsing and rendering

Parallelism is important for performance

I believe the single biggest improvement in the world of web performance was when browsers started downloading scripts in parallel. Before 2006, all browsers downloaded and executed external scripts sequentially. In those old days of sequential script downloading, this markup:

<script src="aphid.js"></script>
<script src="bmovie.js"></script>
<script src="seaserpent.js"></script>
<img src="deejay.gif">
<img src="elope.gif">

would result in this waterfall chart:

aphid.js        ====xxx
bmovie.js              =====xx
seaserpent.js                 =====xx
deejay.gif                           =====
elope.gif                            =====
DOM Interactive                      *
image render                              *
Waterfall 1: pre-2006 sequential script loading

In this handcrafted waterfall chart I’m using “==” to show download time and “xx” to indicate script parse and execution. (I make aphid.js faster to download and slower to execute for reasons that will become clear in a minute.) Notice how after each script is downloaded, there’s a little bit of time to parse and execute the script. DOM Interactive (performance.timing.domInteractive) fires after the browser finishes parsing the HTML document (which includes the time to parse & execute all synchronous scripts). The image render time is an approximation that doesn’t include image decoding and assumes there are no other blocking resources like stylesheets.

There’s a lot of inefficiencies in this old approach. While it’s true that scripts need to be executed sequentially, they can be downloaded in parallel. And scripts definitely don’t need to block other non-script resources, like images.

Starting with IE8 and followed soon after by other browsers, the concept of a preloader (or lookahead parser or speculative parser) was introduced where scripts could be downloaded in parallel. With the implementation of preloaders, pages got INCREDIBLY faster. Here’s the improved waterfall for our example:

aphid.js        ====xxx
bmovie.js       =====  xx
seaserpent.js   =====    xx
deejay.gif      =====
elope.gif       =====
DOM Interactive            *
image render               *
Waterfall 2: benefits of the preloader

In this waterfall that benefits from the browser preloader, we see that all the resources are downloaded in parallel. (In some browsers, the images might be given a lower priority and start downloading a bit later.) DOM Interactive still has to wait for all three scripts to parse and execute, but that happens much earlier which means the images are shown more quickly resulting in a happier user experience.

Synchronous scripts block HTML parsing

Preloaders improve web performance by changing it so synchronous scripts don’t block other resources from downloading. But synchronous scripts still block the browser’s HTML parser: when the HTML parser reaches a SCRIPT tag it stops until that script has been downloaded (if it’s an external script), parsed, and executed. When the HTML parser is blocked it means the user has to wait to see the content on the page. That’s why deejay.gif and elope.gif in the previous waterfall are blocked from rendering until all the scripts are done parsing and executing.

To avoid synchronous scripts blocking the HTML parser, developers started figuring out ways to load scripts asynchronously. Not ALL scripts should be loaded asynchronously. If the script is needed to render the critical content on the page, then it should be loaded synchronously. But if the script isn’t needed for critical rendering, then it can be loaded asynchronously. In the old days (2009), we had hacks for loading scripts asynchronously. Then the ASYNC and DEFER attributes were added to the HTML spec.

ASYNC and DEFER

ASYNC and DEFER are similar in that they allow scripts to load without blocking the HTML parser which means users see page content more quickly. But they do have differences:

  • Scripts loaded with ASYNC are parsed and executed immediately when the resource is done downloading. Whereas DEFER scripts don’t execute until the HTML document is done being parsed (AKA, DOM Interactive or performance.timing.domInteractive).
  • ASYNC scripts may load out-of-order, whereas DEFER scripts are executed in the order in which they appear in markup. (Although there’s a bug that makes DEFER’s execution order questionable in IE<=9.)

Even though ASYNC and DEFER don’t block the HTML parser, they can block rendering. This happens when they’re parsed and executed before rendering is complete and take over the browser main thread. There’s nothing in the spec that says they have to wait until rendering is complete. ASYNC scripts execute immediately once they finish downloading, and DEFER scripts execute after DOM Interactive.

To illustrate ASYNC’s behavior, let’s add ASYNC to aphid.js from our earlier example:

<script ASYNC src="aphid.js"></script>
<script src="bmovie.js></script>
<script src="seaserpent.js"></script>
<img src="deejay.gif">
<img src="elope.gif">

Here’s what the ASYNC waterfall looks like:

aphid.js        ====xxx
bmovie.js       =====  xx
seaserpent.js   =====    xx
deejay.gif      =====
elope.gif       =====
DOM Interactive            *
image render               *
Waterfall 3: aphid.js ASYNC

Even though aphid.js is ASYNC, it blocks the other scripts from executing because it finished downloading first. In other words, an ASYNC script blocks all synchronous scripts that finish downloading after it (Cuzillion test).

To see how DEFER works, let’s add it to the example markup:

<script DEFER src="aphid.js"></script>
<script src="bmovie.js></script>
<script src="seaserpent.js"></script>
<img src="deejay.gif">
<img src="elope.gif">

Here’s what the DEFER waterfall looks like:

aphid.js        ====     xxx
bmovie.js       =====xx
seaserpent.js   =====  xx
deejay.gif      =====
elope.gif       =====
DOM Interactive          *
image render             *
Waterfall 4: aphid.js DEFER

Because DEFER’ed scripts are executed after DOM Interactive, the synchronous scripts in the page (bmovie.js and seaserpent.js) are able to execute first even though they finish downloading later. Comparing the ASYNC and DEFER waterfalls, we see that using DEFER makes DOM Interactive fire sooner and allows rendering to proceed more quickly.

Why prefer DEFER

The waterfalls above illustrate how ASYNC and DEFER cause JavaScript execution to happen at different times in your page. DEFER always causes script execution to happen at the same time as or later than ASYNC. Presumably, scripts are made DEFER or ASYNC because they are less important for the critical content on the page. Therefore, it’s better to use DEFER so that their execution happens outside of the main rendering time.

DEFER scripts can never block synchronous scripts, while ASYNC scripts might depending on how quickly they download. Synchronous scripts are typically made synchronous because they are important for the critical content on the page. Therefore, it’s better to use DEFER so that synchronous scripts are not blocked from executing and their critical work is completed more quickly.

My waterfalls above are hypothetical, but it’s not too hard to find occurrances of ASYNC’s blocking behavior in the wild. Instagram has a small async script on their front page. In this WebPageTest result for Instagram that ASYNC script is request 4. Looking at the Timeline we see that the ASYNC script is executed before all the synchronous scripts around 0.6s into the page, way before DOM Interactive at 1.2s.

To see the benefits of DEFER on a live site we can look at Yelp. Their WebPageTest result includes a DEFER script as the second request. This script finishes downloading at 0.6s but doesn’t execute until DOM Interactive at 1.5s, thus allowing the other synchronous scripts in the page to be executed first and get rendering to happen sooner.

What it comes down to is the value of delaying DEFER script execution until DOM Interactive versus risking ASYNC scripts executing sooner if they download quickly. Thus, it’s important to know your DOM Interactive time. For the Alexa Top 100, the median DOM Interactive time is 2.1 seconds and the 95th percentile is 11.2 seconds (based on HTTP Archive data from Nov 15 2016). That value is large enough to create a window where ASYNC scripts could finish downloading before DOM Interactive and end up causing a further delay to page rendering.

If you use ASYNC on any of your scripts, do a test to see if DEFER makes rendering happen sooner. If you have a high DOM Interactive time, you might be pleasantly surprised at the results.