How, When, And Why Script Loaders Are Appropriate

Alex Sexton | June 1, 2010

 

Script loaders are all the rage these days. Every time someone releases a new one, it's met with both excitement and groans of 'so-and-so already did that' on the Twittersphere. It is indicative of the need we have, and the complexity of the problem. The outcome is a huge awareness of the need of script loaders and a lot of misinformation about when or why they're appropriate. Being in the not-so-elite group of people who have written a script loader, I'd like to set some records straight, and try to step on as few toes along the way.

Do I need a script loader?

Absolutely not, script loaders are often about either 1) increasing the loading speed on your page, or 2) helping you organize your large application. Sometimes both, but usually the two are fairly incompatible (at a very micro-level). Browsers, especially modern ones, are not so bad at loading scripts if you put them in the right places.

In most cases, just concatenating all of your scripts together is going to give you speeds anywhere from almost-as-good-as-a-script-loader to way-way-faster-than-a-script-loader depending on the situation. So when people talk about the benefits of a script loader, we're talking about eeking out the last little bits of performance, not revolutionizing your page load time. If you want the quickest win, just concatenate your scripts together, and place them at the bottom of your page. That is going to be the biggest jump in performance 99 times out of 100.

The most generic of script loaders (of which there are many) simply create a script tag element and inject it into your page. Since the DOM element that it was injected into already exists, it logically cannot 'block' the rendering of the rest of the page so the browser treats it as if it were asynchronous. This is the way the asynchronous Google Analytics script, which many people are used to including on their page, works.

// Very simple asynchronous script-loader

// Create a new script element
var script      = document.createElement('script');

// Find an existing script element on the page (usually the one this code is in)
var firstScript = document.getElementsByTagName('script')[0];

// Set the location of the script
script.src      = "https://example.com/myscript.min.js";

// Inject with insertBefore to avoid appendChild errors
firstScript.parentNode.insertBefore( script, firstScript );

// That's it!

One of the benefits of loading scripts asynchronously, is that you can load multiple scripts at the same time. In most browsers, just including a script tag will cause the rest of the page to stop rendering, will wait for the script to load, execute, and then continue on with what it's doing. This is generally bad (and why you should always include scripts at the bottom, whenever possible). Asynchronous scripts don't block so you can load more than one at a time. HTTP requests are known to have considerable overhead, so doing these things in parallel actually reduces the cost of multiple HTTP requests. Some would argue that these scripts would just load at the same time, but twice as slow, but in practice that is generally not the case. Some pages have seen a measurable improvement by loading more than one script at the same time.

So at this point we've determined that concatenating your scripts and putting them at the bottom of the page is going to give you the biggest jump in performance. But if we load a script asynchronously, we may gain a small advantage by not blocking other things on the page. So some would argue that asynchronously loading a single concatenated script into your page would be the best performance case. They are often correct.

The Third Party

"But what about loading scripts from CDNs and other sites?" -- Exactly. The web is made up of tons of third party scripts, and whether you want the speed of an edge-cached CDN, or you're including a third party widget from a different domain, you can't concatenate them all together. This, in my opinion, is almost the only reason to have multiple script requests on a single page load. We now have the desire to load in a few scripts, from different places, into our page as fast as possible. (Just as an aside though, some very smart people would argue that you should just pull in third party scripts on the server-side and concatenate them into your single file.)

The hard part about asynchronous script loading is that the scripts generally execute in the order that they finish loading. This is bad if your scripts depend on each other and the one that should run second finishes loading first. So most script loaders attempt to address the execution order of asynchronously loaded scripts. There are a few ways to do this. Some browsers will automatically execute in the correct order if you give the script tag the 'async=false' attribute. This is new functionality, though, so you still have to have a fallback. Other script loaders try to load the scripts into an Object tag or an IMG tag in order to cache, but not execute it - only to inject the script again in an actual script tag and to force it to load from cache, causing it to begin to execute immediately. Still, other script loaders just wait until one script is done executing before beginning to load the other - though this is usually for development, and then the scripts are packaged up into a single request for production. There are other hacks as well that include XHR and pulling out comment blocks and faking script type attributes.

The bottom line is that there are good and bad things about each of the methods for asynchronously loading multiple scripts in parallel, and executing them in order. You have to pick which downsides of each individual loader you are willing to deal with in exchange for a potential speed boost. When using script caching techniques, correct caching headers are required, and different branches must be made for different browsers. When using XHR, you must be on the same domain as the file, which eliminates 3rd party scripts. When using the Object tag to preload scripts, line-breaking algorithms are sometimes run against the script text itself. Not to mention, since browser support for all this is fairly shaky, most script loaders have varying policies and success rates on different browsers that are important to you.

The thing that I see many people overlook though, is that a script loader is not a replacement for concatenating (and/or minifying) your JavaScript. I see too many sites that have 10 asynchronous parallel requests. This will usually be significantly slower than even just including 10 script tags, I couldn't recommend against it more. In the real world, 3 scripts loading in parallel seems to be the maximum that anyone has found benefit for.

Now that I've hopefully fully convinced you to always concatenate as many of your scripts as possible, I'd like to show you one reason why you might want to avoid this in special cases. If your page is using modern browser features, you likely have conditional scripts that only run in the case of a lack of browser support. These are usually called 'polyfills' but they can be other things as well. In newer/supporting browsers much of this code is essentially a dead code path (it'll never get run). That's a lot of loading and parsing and memory use just to do nothing. That's where conditional loading comes in. While concatenating together all of your files can make things load faster (because of the decreased overhead of multiple requests), leaving out large chunks of code altogether can be even better on your performance. The library I created (called yepnope.js) was created for this purpose specifically, however most script loaders can do conditional loading with minimal effort.

Yepnope works a little bit like this:

yepnope([
  {
    test: window.JSON,
    nope: 'js/json2.js',
    complete: function () {
      console.log( window.JSON.parse( '{ "JSON" : "HERE" }' ) );
    }
  }
]);

In this example, we avoid loading the json2.js file if we have native JSON available on our page. This is not an insignificant amount of savings.

In RequireJS, if you had a JSON module, it might look something like this:

require( [ window.JSON ? undefined : 'util/json2' ], function ( JSON ) {
  JSON = JSON || window.JSON;

  console.log( JSON.parse( '{ "JSON" : "HERE" }' ) );
});

That's a little clunkier, and you may have to mess with your build a little, but it's totally possible. Alternatively, you could wrap the window.JSON variable in a module, and require it conditionally as well.

(Not) Choosing a Script Loader

So at this point I'd hope you understand why script loaders exist and when or when not to consider using one on your project. I'd also hope you understand how important just minifying and concatenating your scripts can be (as out of date as the tip may seem). If you do decide that you'd like to use a script loader in order to eek out that last bit of performance, you'll likely feel lost on which one you should choose. I'd be weary of any feature-by-feature comparison of script loaders. Often times script loaders have different features missing or available very much on purpose, and a proper checklist is a bit of a misnomer.

First, I'd suggest knowing how big the scope of your application will be, and how big it might get in the future. This way you can break the list of loaders more or less in half. There are the dependency management loaders like RequireJS, LoadRunner, Dojo, and CurlJS. And there are the smaller more generic script loaders like LabJS, YepnopeJS and A.getJS. The dependency management options generally require some adherence to a module system to actually get the benefit of dependency management, and the generics just load scripts (or avoid loading them) in the order that you provide.

I can't make the actual decision for you. You'll need to look into things like caching headers that are required, memory footprint, fallback support, etc. and grade the loader that you think best fits your requirements. I would however suggest that you get the class of loader correct, as I would not suggest people use Yepnope for super large applications, and RequireJS might be overkill for a small splash page.

How Could I Know?

It's easy. Test. Profile. Look.

So many people are content to add a script loader to their page without testing to see if it's helping them at all. I'd implore you to open up your development tools and check the loading speeds of your page with and without various script loaders. There are so many varying combinations of situations that it's impossible to know when one method will work better than another. This doesn't take that much time and if you don't do it, I'd recommend just avoiding script loaders altogether, because there is much more consistency and stability in native script inclusion.

What's Next?

Ideally, the recent surge of script loaders will throw up a flag in the standards bodies. Already the work of people like Kyle Simpson has brought some of the script loader mentality (via async=false) into the specification organizations. It'd be great to see more of this happening, so all future browsers won't have to rely on tricks to do in-order-asynchronous-parallel-script-loading. A mouthful, I know. So call your local congress(man|woman) and ask him/her to put in a good word with Tim Berners-Lee or something.

 

About the Author

Alex is a Labs Engineer at Bazaarvoice in Austin, TX. He is a co-host of the yayQuery Podcast and is a founding member of The Society For The Fair Treatment of JavaScript. OK, not really, but he thinks it's a pretty cool language and he wants you to use it right! Apparently, he also talks in third person when writing bios.

Find Alex on: