Load scripts async

THURSDAY 20TH OF DECEMBER 2018

This blog post has a simple conclusion: Load script asynchronously! Simple, and yet the reality is that most scripts are still loaded synchronously. Understanding the importance of loading scripts asynchronously might help increase adoption of this critical performance improvement, so we're going to walk through the evolution of async script loading starting way back in 2007. Here's what loading 14 scripts looked like in Internet Explorer 7:

IE7 Waterfall

I've added a red arrow at the beginning of each script request. What we see is that the HTML parser stops when it encounters a SCRIPT tag until that script is downloaded, parsed, and executed. Because the HTML parser stopped, all network traffic also stopped (because no other HTML tags were parsed to initiation HTTP requests). It wasn't just IE7 that did this - all browsers behaved this way back then.

Enter the Preloader

While it's necessary that scripts get executed in the order specified, there's no reason they can't be downloaded in parallel so that the execution daisy chain moves more quickly. To achieve this, browsers created a lightweight parser called the preloader. The preloader (AKA, speculative or lookahead parser) skims over the HTML looking for tags that might initiate HTTP requests, like SCRIPT, LINK, and IMG. It prioritizes and initiaties those requests so that the responses are more likely to be ready when the HTML parser actually creates the corresponding DOM elements.

Here's a site with 15 synchronous scripts (also primarily from turner.com). Instead of taking 6 seconds like it did in IE7 with one-at-a-time script downloading, the preloader helps get these scripts downloaded in ~1.2 seconds. (Notes about this waterfall chart: The rendering filmstrip is shown at the top. The browser CPU timeline is shown below the waterfall. Scripts are shown in orange. Synchronous scripts are shown with a red hash pattern. The red and green bars to the right of each script are when the script uses the CPU. Total CPU time for each script is shown in parentheses after the URL.)

The preloader is the most important performance improvement browsers ever made. IE8 was the first browser to have a preloader in 2009. Most of the other major browsers followed suit within the year. 

While the preloader dramatically improved web performance, it wasn't a panacea. Looking at the previous waterfall, we see that:

  • Rendering is blocked until all the synchronous scripts are done downloading, parsing, and executing. If they were downloaded asynchronously, the browser could start rendering sooner. 
  • The second script, jquery.js, takes a long time to download. Because it's loaded synchronously, all the subsequent scripts have to wait to be executed, even though they finish downloading before jquery.js. This results in the browser CPU being idle for the first 1.2 seconds and then being pegged when all the JS and layout happens. If the scripts were downloaded asynchronously, they could be parsed and executing immediately when they arrive spreading out the CPU activity which would avoid blocking the CPU.
  • The preloader gives synchronous scripts a higher download priority than images. Since the synchronous scripts take so long to download, the image requests are blocked and the initial rendering only includes text. If the scripts were loaded asynchronously, images would get downloaded sooner providing a better user experience.

Finally: Async

Looking at the previous waterfall, we see that the preloader dramatically improved performance and rendering. However, it didn't solve all the problems. That page has 15 synchronous scripts, which block rendering, peg the CPU, and delay images. The solution to these problems is to load scripts asynchronously.

The best way to load scripts asynchronously is to use the async attribute. Browsers added support for this attribute starting with Firefox and Chrome in 2010, followed by Safari and Internet Explorer in 2012. Before these were widely supported, developers could load scripts asynchronously using a variety of techniques (i.e., hacks). These techniques are still used today for special situations, but it's simpler to use async in markup. It's also better for performance, as the preloader can spot these scripts and get the HTTP requests started sooner than if they are loaded programmatically.

async timeline

The benefits of loading scripts asynchronously are highlighted in the following waterfall chart for nytimes.com. Notice that none of the scripts have the red hash pattern that marks a synchronous script, which means all of these 20+ scripts are loaded asynchronously. As a result, the page renders quickly (0.7 seconds) and the script parsing and execution are not blocked - each script is handled as soon as it arrives.

Async waterfall

Async Adoption

It's great that the async attribute is available and well known, but synchronous script loading is still the default. In other words, website owners need to make an extra effort to get their scripts to load asynchronously. It might not be a lot of work (just add the async attribute) but even small changes can take a long time to reach critical mass on the Web. 

The chart below shows the adoption of asynchronous script loading. These are median stats from the world's top ~500K websites based on an analysis from the HTTP Archive. In late 2015, the median was 10 synchronous scripts and 2 asynchronous scripts. The number of sync dropped and async rose in June 2016. This is likely when some dominant third party snippets (Google Analytics, Facebook, etc.) switched from sync to async. The total number of scripts has continued to climb since then, but all the new scripts use async, which is a great sign. Nevertheless, the most recent data shows that synchronous scripts outnumber asynchronous scripts 7 to 6. 

This growth in the number of scripts loaded asynchronously is impressive. Three years is a long time, but we're talking about half-a-million websites. While the change to make synchronous scripts be asynchronous is trivial (just add the async attribute), making sure that change doesn't produce errors can be challenging. If JavaScript is parsed and executed out-of-order, undefined symbol errors are likely. Untangling the interdependencies to ensure safe async script loading may take time, but the performance benefits are worth it, for you and your users.

comments powered by Disqus