Script-injected "async scripts" considered harmful
Synchronous scripts are bad because they force the browser to block DOM construction, fetch the script, and execute it before the browser can continue processing the rest of the page. This, of course, should not be news, and is the reason why we have been evangelizing the use of asynchronous scripts. Here is the canonical example:
<!-- BAD: blocking external script --> <script src="//somehost.com/awesome-widget.js"></script> <!-- GOOD: remote script is loaded asynchronously --> <script> var script = document.createElement('script'); script.src = "//somehost.com/awesome-widget.js"; document.getElementsByTagName('head').appendChild(script); </script>
What’s the difference? In the “bad” example we block DOM construction, wait to fetch the script, execute it, and then continue to process the rest of the document. In the second example, we begin executing the inline script, which creates a script element pointing to an external resource, add it to the document, and continue processing the DOM. The difference is subtle but very important: script-injected scripts do not block on the network.
Script-inject all the things? Not so fast.
The above page loads a CSS file at the top of the page and two script-injected “async scripts” at the bottom. In other words, it follows all of the “performance best practices”. Except, the scripts themselves can’t be executed until the CSSOM is ready, which delays the execution of the inline blocks and consequently the dispatch of the network requests. As a result, the scripts are executed ~3.5s after the page request is initiated.
Now, let’s compare this to our “bad” example, which uses two blocking script tags:
Wait a second, what’s going on? Both scripts are fetched earlier and are executed ~2.7 seconds after the page request is initiated. Note that the scripts are still executed only once the CSS is available (~2.7 second mark), but because the scripts are already fetched by the time the CSSOM is ready, we can execute them immediately, saving over a second in our processing times. Have we been doing it all wrong?
Before we answer that, let’s consider one more example, this time with the “async” attribute:
<script src="http://udacity-crp.herokuapp.com/time.js?rtt=1&a" async></script> <script src="http://udacity-crp.herokuapp.com/time.js?rtt=1&b" async></script>
If the async attribute is present, then the script will be executed asynchronously, as soon as it is available. If the async attribute is not present ... then the script is fetched and executed immediately, before the user agent continues parsing the page.
The async attribute on the script tag provides two critical properties: it tells the browser to not block DOM construction, and it does not block script execution on CSSOM. As a result, the scripts are executed as soon as they are downloaded (at ~1.6 seconds) and without having to wait for the CSSOM. A quick summary of our results:
So, why have we been advocating the use of this pattern for so long?
“async” is not supported by a few older browsers: IE 8/9, Android 2.2/2.3. As a result, these older browsers ignore the attribute and treat it as a blocking script. This was a big problem back in the day, but this also brings us to the next point…
All modern browsers have a “preload scanner” (yes, even IE8/9 and Android 2.3/2.2) which is invoked when the document parser is blocked and whose sole responsibility is to “peek ahead” in the document and find resources that should be fetched as soon as possible to unblock the critical rendering path.
The script-injected pattern offers no benefits over
<script async>. The reason it exists is because
<script async> was not available and preload scanners did not exist back when it was first introduced. However, that era has now passed, and we need to update our best practices to use async attribute instead of script-injected scripts. In short, script-injected “async scripts” considered harmful.
Also, note that the preload scanner will only discover resources that are specified via
<!-- BAD: the pre async / pre preload scanner era --> <script> var script = document.createElement('script'); script.src = "//somehost.com/awesome-widget.js"; document.getElementsByTagName('head').appendChild(script); </script> <!-- GOOD: simpler, faster, and better all around --> <script src="//somehost.com/awesome-widget.js" async></script>
|Blocks DOM construction||Does not block DOM construction|
|Execution is blocked on CSSOM||Execution is not blocked on CSSOM|
|Preload scanner discoverable||Preload scanner discoverable|
|Ordered execution of scripts||Unordered execution|
|Use where execution order matters, place these scripts at the bottom.||Can be placed anywhere, ideal for scripts that can tolerate out-of-order execution.|
What about the defer attribute?
defer was introduced prior to
async and, in theory, was supposed to guarantee that the scripts would not block the parser, and would be executed before
DOMContentLoaded event in the order they were inserted. Unfortunately, in practice, the execution order implementation is buggy and cannot be relied on. That said,
defer is still a useful attribute as it unblocks the document parser in older IE browsers (5.5-9) that do not support the
async attribute. As a result, we can combine
defer to further improve performance.
<!-- Modern browsers will use 'async', older browsers will use 'defer' --> <script src="//somehost.com/awesome-widget.js" async defer></script>
The defer attribute may be specified even if the async attribute is specified, to cause legacy Web browsers that only support defer (and not async) to fall back to the defer behavior instead of the synchronous blocking behavior that is the default.
But wait, what about…
Async attribute makes no guarantees about execution order: scripts are executed as they arrive, their order and location in the document has no effect. As a result, if you have dependencies, can you eliminate them? Alternatively, can you defer their execution, or make the order of execution a non-issue? The asynchronous function queuing pattern is a good one to investigate.
<head>- then the inline block is executed immediately. The problem with inline blocks is that they must block on CSSOM, but if we place them before any CSS declarations, then they are executed immediately.
<head>lean to allow the browser to discover your CSS and begin parsing the actual page content as soon as possible - i.e. optimize the content you deliver in your first round trip to enable the fastest possible page render.
defer+asyncto improve performance in older browsers.
Sep 29, 2014: Google+ widgets have been updated to provide