Evolution of Script Loading
Velocity China starts tomorrow morning. I’m kicking it off with a keynote and am finishing up my slides. I’ll be talking about progressive enhancement and smart script loading. As I reviewed the slides it struck me how the techniques for loading external scripts have changed significantly in the last few years. Let’s take a look at what we did, where we are now, and try to see what the future might be.
Scripts in the HEAD
Just a few years ago most pages, even for the top websites, loaded their external scripts with HTML in the HEAD of the document:
<head> <script src=â€core.js†type=â€text/javascriptâ€></script> <script src=â€more.js†type=â€text/javascriptâ€></script> </head>
Developers in tune with web performance best practices cringe when they see scripts loaded this way. We know that older browsers (now that’s primarily Internet Explorer 6 & 7) load scripts sequentially. The browser downloads core.js and then parses and executes it. Then it does the same with more.js – downloads the script and then parses and executes it.
In addition to loading scripts sequentially, older browsers block other downloads until this sequential script loading phase is completed. This means there may be a significant delay before these browsers even start downloading stylesheets, images, and other resources in the page.
These download issues are mitigated in newer browsers (see my script loading roundup). Starting with IE 8, Firefox 3.6, Chrome 2, and Safari 4 scripts generally get downloaded in parallel with each other as well as with other types of resources. I say “generally†because there are still quirks in how browsers perform this parallel script loading. In IE 8 and 9beta, for example, images are blocked from being downloaded during script downloads (see this example). In a more esoteric use case, Firefox 3 loads scripts that are document.written into the page sequentially rather than in parallel.
Then there’s the issue with rendering being blocked by scripts: any DOM elements below a SCRIPT tag won’t be rendered until that script is finished loading. This is painful. The browser has already downloaded the HTML document but none of those Ps, DIVs, and ULs get shown to the user if there’s a SCRIPT above them. If you put the SCRIPT in the document HEAD then the entire page is blocked from rendering.
There are a lot of nuances in how browsers load scripts especially with the older browsers. Once I fully understood the impact scripts had on page loading I came up with my first recommendation to improve script loading:
Move Scripts to the Bottom
Back in 2006 and 2007 when I started researching faster script loading, all browsers had the same problems when it came to loading scripts:
- Scripts were loaded sequentially.
- Loading a script blocked all other downloads in the page.
- Nothing below the SCRIPT was rendered until the script was done loading.
The solution I came up with was to move the SCRIPT tags to the bottom of the page.
... <script src=â€core.js†type=â€text/javascriptâ€></script> <script src=â€more.js†type=â€text/javascriptâ€></script> </body>
This isn’t always possible, for example, scripts for ads that do document.write can’t be moved – they have to do their document.write in the exact spot where the ad is supposed to appear. But many scripts can be moved to the bottom of the page with little or no work. The benefits are immediately obvious – images download sooner and the page renders more quickly. This was one of my top recommendations in High Performance Web Sites. Many websites adopted this change and saw the benefits.
Load Scripts Asynchronously
Moving scripts to the bottom of the page avoided some problems, but other script loading issues still existed. During 2008 and 2009 browsers still downloaded scripts sequentially. There was an obvious opportunity here to improve performance. Although it’s true that scripts (often) need to be executed in order, they don’t need to be downloaded in order. They can be downloaded in any order – as long as the browser preserves the original order of execution.
Browser vendors realized this. (I like to think that I had something to do with that.) And newer browsers (starting with IE8, Firefox 3.6, Chrome 2, and Safari 4 as mentioned before) started loading scripts in parallel. But back in 2008 & 2009 sequential script loading was still an issue. I was analyzing MSN.com one day and noticed that their scripts loaded in parallel – even though this was back in the Firefox 2.0 days. They were using the Script DOM Element approach:
var se = document.createElement("script"); se.src = "core.js"; document.getElementsByTagName("head")[0].appendChild(se);
I’ve spent a good part of the last few years researching asynchronous script loading techniques like this. These async techniques (summarized in this blog post with full details in chapter 4 of Even Faster Web Sites) achieve parallel script loading in older browsers and avoid some of the quirks in newer browsers. They also mitigate the issues with blocked rendering: when a script is loaded using an async technique the browser charges ahead and renders the page while the script is being downloaded. This example has a script in the HEAD that’s loaded using the Script DOM Element technique. This script is configured to take 4 seconds to download. When you load the URL you’ll see the page render immediately, proving that rendering proceeds when scripts are loaded asynchronously.
Increased download parallelism and faster rendering – what more could you want? Well…
Async + On-demand Execution
Loading scripts asynchronously speeds up downloads (more parallelism) and rendering. But – when the scripts arrive at the browser rendering stops and the browser UI is locked while the script is parsed and executed. There wouldn’t be any room for improvement here if all that JavaScript was needed immediately, but websites don’t use all the code that’s downloaded – at least not right away. The Alexa US Top 10 websites download an average of 229 kB of JavaScript (compressed) but only execute 29% of those functions by the time the load event fires. The other 71% of code is cruft, conditional blocks, or most likely DHTML and Ajax functionality that aren’t used to render the initial page.
This discovery led to my recommendation to split the initial JavaScript download into the code needed to render the page and the code that can be loaded later for DHTML and Ajax features. (See this blog post or chapter 3 of EFWS.) Websites often load the code that’s needed later in the window’s onload handler. The Gmail Mobile team found wasn’t happy with the UI locking up when that later code arrived at the browser. After all, this DHTML/Ajaxy code might not even be used. They’re the first folks I saw who figured out a way to separate the download phase from the parse-execute phase of script loading. They did this by wrapping all the code in a comment, and then when the code is needed removing the comment delimiters and eval’ing. Gmail’s technique uses iframes so requires changing your existing scripts. Stoyan has gone on to explore using image and object tags to download scripts without the browser executing them, and then doing the parse-execute when the code is needed.
What’s Next?
Web pages are getting more complex. Advanced developers who care about performance need more control over how the page loads. Giving developers the ability to control when an external script is parsed and executed makes sense. Right now it’s way too hard. Luckily, help is on the horizon. Support for LINK REL=”PREFETCH” is growing. This provides a way to download scripts while avoiding parsing and execution. Browser vendors need to make sure the LINK tag has a load event so developers can know whether or not the file has finished downloading. Then the file that’s already in the browser’s cache can be added asynchronously using the Script DOM Element approach or some other technique only when the code is actually needed.
We’re close to having the pieces to easily go to the next level of script loading optimization. Until all the pieces are there, developers on the cutting edge will have to do what they always do – work a little harder and stay on top of the latest best practices. For full control over when scripts are downloaded and when they’re parsed and executed I recommend you take a look at the posts from Gmail Mobile and Stoyan.
Based on the past few years I’m betting there are more improvements to script loading still ahead.
Diego Perini | 06-Dec-10 at 10:24 am | Permalink |
The lazy loading after “onload” you attribute to GMail is something we have been using in our product since 2005:
[body onload=”(function(){var d=document,s=d.createElement(‘script’);s.src=’/js/nwbase.js’;d.getElementsByTagName(‘head’)[0].appendChild(s)})()”]
see here:
http://web.archive.org/web/20060217170500/http://www.iport.it/index.html
For the IFRAME loading technique, the following was talked about in Dean Edwards blog in 2006 too:
http://javascript.nwbox.com/JsCOPE/jscope.html
At the time I was working on Firefox 1.5 and Internet Explorer 6 and this still work in newer versions of Firefox.
Some other browser like Opera also works with this older code but I have been working on a newer more capable version lately. Will release it soon.
I have investigated the PNG loading technique with good results and recently released a Javascript gist to aid in the conversions.
Also, I believe loading script in IFRAMES without modify them is doable, obviously it will help if script where written consciously about these facts. I am working on that.
As you can see in my examples loading a library in an IFRAME and using it through a reference is possible if the loaded framework as knowledge of context. I should patch the example a bit since older versions of Mootools and Prototype had no knowledge of “context”.
This type of loading have several advantages in my point of view it is not just because it makes the loading asynchronous and faster. Running things in a separate sandbox has many advantages (as we have seen in later developments).
Peter Cranstone | 06-Dec-10 at 12:05 pm | Permalink |
Steve,
Great post. So how can we measure the performance results due to these changes? Where are the actual numbers that show the improvement on Mobile? Do the Goggle Mobile guys have some numbers they can share?
Thanks,
Peter
Steve Souders | 06-Dec-10 at 1:42 pm | Permalink |
@Diego: Great references. I look forward to hearing the results of your next technique.
@Peter: In the Gmail blog post they describe reducing startup time from 2300 ms.
Mark Nottingham | 06-Dec-10 at 8:19 pm | Permalink |
I think you mean link rel=”prefetch”, no?
Steve Souders | 06-Dec-10 at 9:25 pm | Permalink |
@Mark: You’re right – corrected. Thank you!
Stephane | 07-Dec-10 at 6:54 am | Permalink |
I think you mean appendChild(se), right?
YuBing | 07-Dec-10 at 8:28 am | Permalink |
you missed the http:// in the velocity china’s link~~~
Steve Souders | 07-Dec-10 at 1:55 pm | Permalink |
@Stephane & @YuBing: Thanks for catching the mistakes. I wrote this post at midnight my first day in China. A little bleary eyed.
mike | 08-Dec-10 at 1:22 am | Permalink |
Thoughts on http://headjs.com/ ?
Wladimir | 09-Dec-10 at 5:35 am | Permalink |
Great post, it was an interesting read, I had no idea so much research was going into where to put the script loading.
@”There wouldn’t be any room for improvement here if all that JavaScript was needed immediately, but websites don’t use all the code that’s downloaded – at least not right away”
This saddens me, 30+ years after the fact we’re still re-inventing paging on demand. In any low-level language such as C this is handled for you by the OS, but in Javascript we need be tricky and implement it ourselves.
Steve Souders | 11-Dec-10 at 10:18 am | Permalink |
@mike: I looked at how headjs loads scripts. It does indeed reduce the blocking effect of scripts, but there are several things about its technique that could be improved:
1. The head.min.js script itself is loaded in a blocking fashion. Instead, it should be loaded non-blocking.
2. It doesn’t handle inline scripts which could result in undefined symbol errors.
3. Script download precedence isn’t preserved. Scripts specified first are loaded last (after other resources such as images). Users often want scripts to be downloaded ASAP but just not block other resources in the page.
4. It doesn’t provide the ability to download a script but delay the parsing & executing of the script.
Tero Piirainen | 11-Dec-10 at 9:05 pm | Permalink |
@steve: I’m the author of Head JS and here are my comments on your points
1) wrong: all scripts just cannot be loaded in non-blocking fashion: namely those that affect on styling. like modernizr or head js. you want to avoid FOUC.
2) wrong: you can supply inline scripts as a callback and they are executed once the dependencies arrive
3) wrong: scripts are loaded immediately when a head.js() call is made and they are loaded in parallel and they arrive as soon as they are loaded. or what do you mean? how did you came up into this conclusion?
4) yes. it does not. however you can supply an execution order for scripts. I consider this as an advanced feature not being used by regular websites too often
Tero Piirainen | 11-Dec-10 at 9:19 pm | Permalink |
As a sidenote: Head JS loads scripts almost like specified in Stoyan’s article:
http://www.phpied.com/preload-cssjavascript-without-execution/
a slightly improved logic can be used since we are only dealing with scripts and not also CSS + Images. Head JS uses OBJECT tag for Firefox and “script/cache” type- attribute with other browsers. Both provides a callback when a script has finished loading and the execution can begin.
Steve Souders | 17-Dec-10 at 5:25 pm | Permalink |
@Tero: HeadJS got me off my duff to release ControlJS. That’s where my ideas were coming from.