Dela via


A Look at Asynchronous Script Downloads

You hear more and more about slow loading pages, and one of the big factors for this is downloading too many resources and immediate execution. Consequently, developers are looking for resource downloads and actions that they can postpone for later execution – such as after the primary part of the page is loaded and functional. So, how can you take advantage of this?

There are several options, but I want to point out today leveraging defer   and async attributes on your JavaScript declarations. Defer was originally developed in IE4, later added to HTML4 spec, and is documented in the HTML5 spec. The W3C says that a script with the attribute defer implies that execution occurs when the page has finished parsing; thus a time savings. So, one could leverage defer today in HTML4 browsers.  However, a new capability has hit the scene in async for HTML5 , and these two attributes can be used together.

The W3C HTML5 specification says “There are three possible modes that can be selected using these attributes. 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 but the defer attribute is present, then the script is executed when the page has finished parsing. If neither attribute is present, then the script is fetched and executed immediately, before the user agent continues parsing the page.” Plenty of capability provided here to delay execution so you can make your web page have a quicker start.

To get a better look at this async attribute, check out the HTML5 Async Scripts example in the IE Test Drive site. It features two tests: “loading independent scripts faster”, and “run dynamic scripts in order”. And there’s plenty to apply here.

Load Independent Scripts Faster

This is a nice and easy to understand sample, and the effect is easily intuitive. Imagine in your HTML that you have a series of SCRIPT tags and then other resources (like an image) to download. These downloads and script execution will occur in the order parsed, which sometimes can lead to poor performance. Consider the following code snippet, where you might have some JavaScript that can wait until later, as well as a image that is required in the startup of the page. While SLOW.JS is downloading and executing, the image request is waiting before the download can even start and makes the page look unresponsive for a moment. And this is the first way that async comes to the rescue.

In the sample, there are two iframes, both pointing to two identical pages except for one item – which is the async attribute on SLOW.JS (which happens to call a web service that has a almost 2 second SLEEP in it).

    1: <script async src="slow.js"></script>
    2: ...
    3: <img src="IE_Logo_256.png"/>

The sample code is setup such that there are two gray empty boxes, one pointing to a page with the above code (labeled With HTML5 async), and the other box pointing to a similar page without the async attribute (labeled Without HTML5 Async). When the test is initiated, you are waiting for the two windows to show a downloaded IE logo to appear, as well as a message that the script executed. In the Without HTML5 Async window, you will have a grey box, empty of the IE logo, and waiting for the web service to return so SLOW.JS can finish. In the With HTML5 Async window, the IE logo appears first and then later the “Scipt Executed” message appears.

image    image

Again, a nice easy sample to understand and apply. But, let’s move onto another valuable async capability.

Run Dynamic Scripts In Order

Sometimes, a web developer will load script later than the loading of the page, and this is done via code. These are called dynamic scripts, and while easy to do, there’s an important lesson in that they load asynchrously. Yes, this is typically a good feature, but what happens if you need to download a set of scripts and they have a dependency that dictates some execution order be followed. Well, async is here to help again, as well as the HTML5 Async Scripts sample page.

To explain this sample, let’s first explain the visual elements of the test, and then we’ll go into the code inner workings.  Similar to the previous test (see above), this test has two windows which will be loaded with the HTML5 logo. To make the test happen, the code depends on a sprite technique of loading different parts of an image using the CSS background-image property, and later controlling which 64x64 pixel subset to load leveraging the background-position property. If all goes as planned, you should get a logo such as the following:

image

However, here’s where things get interesting. The code dynamically declares the 16 calls for each window (one for each 64x64 slice of the image); however, the script calls out to an external service which contains a random SLEEP value. And because there are two windows, this means that 32 script down loads happen followed by 32 script executions. That would be fine, except that the resulting script has to be executed in a certain order for the image to be constructed properly. The HTML is set up such that it is flowing left to right with room for 4 slices across and 4 slices down (which is why the image is 256x256). The function is basically loading DIV’s dynamically to the window without care of location, so the correct order is required. Remember the lesson of dynamic script: execution is asynchronous! So, when you have a dependency on the order of script execution, you could end up with a mess like this:

image

 

While easy to see the visual results of this test, I wanted to understand what all is happening under the hood here; so, I had to take a peek. Thank goodness for IE Developer Tools, and that they exist in the IE10 Platform Preview. At first, I could not see how this was all happening, so let me take you through my investigation.

First, I was trying to understand how the script order was not being followed. I could see that the two windows were loaded with the same function (see snippet below) except for one line of code. And just like the previous example, there’s a “Without HTML5 Async” window and a “With HTML5 Async” window. The WITH window has Line #2 below, while WITHOUT does not. Otherwise, the windows have the same exact code. This is an important part to understand, but it’s simplicity might hide a few things in this example – so more snooping around required.

    1: var script = document.createElement("script");
    2: script.async = false;
    3: script.src = url;
    4: document.head.appendChild(script);

My next stop was trying to look at the resulting image. Several things that I found out: it’s a set of DIV’s, created dynamically, and leveraging the background-position. Based on the slice #, the background position is calculated. You can see the background-position starts at the top left slice, and works it way through the 16 squares.

This snippet is the WITHOUT ASYNC window, and you’ll see that the slices are not in order – thus resulting in a jumbled mess.

image

And then looking at the WITH ASYNC innerHTML, you’ll see that the elements are ordered and that the resulting HTML5 logo image looks correct. So, how are these getting out of order, since I see that the script declarations are created in order?

image

My next challenge was not being able to debug and have breatkpoints – I’m just not implemented in the IE10 preview. So, then I thought I’d prove out seeing the execution and also improve the sample. I’m smart and can add value to the IE Corp guys. Right?

I started by adding console.Log calls so that I could see the order of the execution. I highly recommend adding calls like this to help understand what is happening. First I created a simple console logging function that would work even when I don’t have the console window open:

    1: function consoleLog(msg)
    2: {
    3:     window.console && console.log && console.log(msg);
    4: }

Then, I sprinkled in a few logging calls to my debug message function, specifically targeting the script declaration and the later DIV creation. Background: the dynamic script calls a server, which downloads a script that calls an existing function already downloaded to the client, log() in demo.js, which creates the DIV that contains the image slice. So, based on this, I know that the scripts are declared for both windows in the correct order, and I’ve now verified that the DIV creation is not in order for the WITHOUT ASYNC container.  Below is a snippet of one of my loggings, logged with each DIV creation, that shows how the two images are created. Note: the calls with an ID that had a “t” appended to the sequence number are the calls processed for the WITHOUT window (t stood for traditional, I believe), where the others are for the WITH ASYNC window.  So, after this was all said and done, I still had not connected the dots and my logging was not that helpful this time. But, I still recommend this tactic. (And the IE Corp guys are smarter than this 3rd grader.)

image

So, then I looked at network trace in the Network tab of Developer Tools. I was able to see the results of the 32 script declarations, 16 for each window. See the trace below. Again, note that the ID is the sequence (and correlates to which slice will be generated) and that the “t” represents which window. One will also notice a sec paramater, which appeared to communicate the SLEEP duration to the server (roughly looked like 100ms x sec parameter). You can see this too – run the trace, and hover the mouse over one of the yellow boxes and then you can see the breakdown of the download. After looking at the request duration of each of these, you’ll see a correlation to the sec parameter.

I could see that the downloads request processing started in order. But the big learning here was that due to the SLEEP call in the web service, finishing the download of script did not occur in the same order – and remembering that the declarative script is asynchrously executed, this is where the order of the image slices is impaired. So, you’ll see that the scripts started to download in order but our asynchronous default behavior hurt us because finishing the download and the resulting execution is not controlled by the code – as blocks 2t and 3t were loaded before 0t and 1t. And look at where 4 landed in order; fortunately, this was on the WITH ASYNC window so it had no impact. This was my AH HA moment.

image

In the trace, you can see where the specific scripts are downloaded (note the beginning of the yellow block), but the important part is when the download finishes (the end of the yellow block). And with this view, you can see when they are downloaded in respect to the others. With this and the key fact that when finished downloading , the script will be executed. But with the variable durations, the resulting execution is not in the order that the code required. (I hope that I’m saying this clearly.)

Fortunately, the WITH ASYNC window had script with the async=TRUE entry, which forced those executions to wait until the previously declared script had finished. BAM! This is where I finally caught on. Yes, I’m slow, but I’m betting someone else might look at this and miss the the beauty of this sample, and how it created the problematic execution for the WITHOUT ASYNC window. It was a good learning experience.

Conclusion: the async attribute in the above snippet is hugely important when dynamic script is created with a dependency on other dynamic script.

Feature Detection of Async

Warning: make sure your browser supports this new async feature. The sample checks to see if your browser can handle by using the following feature detection:

    1: function testSupport() {
    2:     var script = document.createElement("script");
    3:     if(typeof script.async == "undefined") {
    4:         //not supported
    5:     } else if(script.async != true) {
    6:         //Script execution order not supported
    7:     }
    8: }

Conclusion

defer and async are powerful additions to your web development toolbox, and are part of the HTML5 standard. async is in the IE10 Platform Preview, so start thinking about how to leverage.