Trying Out LAB.js For Asynchronous JavaScript (Script) Loading
The other weekend, I watched Kyle Simpson talk about Script Loaders at the New York City GothamJS conference. Script loaders (hopefully) provide an API for optimal, asynchronous JavaScript loading with options for dependency management. If this sounds complicated, it is; actually, from the kind of stuff Kyle was discussing, it's actually quite a bit more complicated than you think it is. I haven't played around with script loaders at all. But, with the depth of knowledge that Kyle Simpson brings to the problem domain, I thought it was time to check out his script loader - LAB.js.
Right now, when I load JavaScript files, I either load them with a number of Script tags; or, I concatenate a number of JavaScript files into one file (and then load that file with a Script tag). Using either of these approaches can have a number of performance impacts. I won't go into detail, since I'm not the expert; but, the script tags can block other assets from loading, delay the DOM-ready event, and cause unnecessary overhead (ie. non-relevant scripts). By using an asynchronous Script loader, you can create modular code that is loaded only as needed in the most efficient way possible.
To test Kyle's LAB.js library, I need to create a few JavaScript files that depended on each other in different ways:
- jQuery - The core jQuery library.
- Friend - A representation of a friend. Depends on jQuery.
- Pet - A representation of a Pet. Depends on jQuery.
- CatLover - A Friend who LOVES cats (every kind of cat). Depends on jQuery, Friend, and Pet.
In this scenario, all files depend on jQuery. Friend and Pet do not rely on each other and can be loaded asynchronously. CatLover depends on all the previous scripts. And, while not listed, our page initialization script (an inline script) requires all the aforementioned scripts to be loaded first.
To manage this part synchronous, part asynchronous dependency chain, we'll be using the script() and wait() methods of LAB.js. Script() loads a remote JavaScript file. Wait() ensures that all previous script() calls in the current queue are loaded before the rest of the chain is executed. Wait() can take an optional callback that allows "inline" code to be executed once the preceding remote JavaScript files have been executed.
Both the script() and wait() methods are chainable; however, what they return was a little confusing at first. The documentation states that they return $LAB. But, in my testing, this appeared to be less than straightforward - they don't actually return $LAB; rather, they return the chainable version of $LAB. In the following code demo, this is why I store a reference to the "queue" (ie. the chainable object); the queue reference allows me to break apart my script() and wait() methods to make the code a bit more readable.
Ok, now that you have a vague idea of what we're doing, let's load our script files and then try to associate a CatLover instance with a dog ... something CatLover does not appreciate!
<!DOCTYPE html>
<html>
<head>
<title>Trying Out LAB.js For Asynchronous Script Loading</title>
<!--
Include the LAB.js file - this will crate the $LAB namespace
that we can use to load the rest of our JavaScript files.
-->
<script type="text/javascript" src="../LAB.min.js"></script>
<script type="text/javascript">
// Load the scripts. We're executing this as a self-executing
// function block so I can change the namespace (from $LAB to
// loader) - for yucks :)
(function( loader ){
// Load the scripts. All scripts depend on jQuery. Then,
// only the cat-lover script depends on the friend.js to
// be loaded (so it can extend it).
var queue = loader
.script( "../../jquery-1.6.1.js" )
.wait()
.script( "./friend.js" )
.script( "./pet.js" )
.wait()
.script( "./cat-lover.js" )
;
// When all the scripts are loaded, init the page.
// Note that we have to call the wait() on the return
// value of the previous chain - otherwise, it seems a
// completely different queue is created.
queue.wait(
function(){
// Create two pets (a dog and a cat).
var piggie = new Pet( Pet.DOG, "Piggie" );
var mrMittens = new Pet( Pet.CAT, "Mr. Mittens" );
// Now create a cat lover (a sub-class of Friend).
var sarah = new CatLover( "Sarah" );
// Try to set the pets.
try {
// Try to set the cat.
sarah
.setPet( mrMittens )
.setPet( piggie )
;
} catch( error ){
// Log the error.
console.log( "Error: ", error.message );
}
}
);
})( $LAB );
</script>
</head>
<body>
<h1>
Trying Out LAB.js For Asynchronous Script Loading
</h1>
</body>
</html>
As you can see, we use the script() method to load a script and then we use the wait() method to enforce order. script() calls after a wait() method will not be executed until the scripts before the wait() method have been executed. In this particular setup, we don't know if "friend.js" will load before or after "pet.js"; since there is no wait() call between them, they will be loaded and executed in the quickest order possible.
Once the remote scripts have all been defined, I then call wait() one more time on the script queue in order to execute my inline page initialization. By passing a callback to the final wait() method call, I can ensure that this inline script gets called only after all of the remote scripts have been loaded.
And, once the scripts have been loaded, we create a CatLover instance, and two pets; then we try to associate the Dog with the CatLover, which throws the following error:
Error: InsufficientPetAwesomeness
Let's take a look at the remote JavaScript files to see why this happened (as expected). First, let's look at the Friend class:
friend.js
// Define the Friend class.
window.Friend = (function( $ ){
// I am an internal counter for ID.
var instanceID = 0;
// I am the class constructor.
function Friend( name ){
// Get the instance ID.
this._id = ++instanceID;
// Store the name value.
this._name = name;
// Store a default value for pet.
this._pet = null;
}
// Define the prototype.
Friend.prototype = {
// I return the id.
getID: function(){
return( this._id );
},
// I return the name.
getName: function(){
return( this._name );
},
// I return the current pet.
getPet: function(){
return( this._pet );
},
// I set the new name.
setName: function( name ){
// Store the new value.
this._name = name;
// Return this object reference for method chaining.
return( this );
},
// I set the new pet.
setPet: function( pet ){
// Store the new value.
this._pet = pet;
// Return this object reference for method chaining.
return( this );
}
};
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Return the constructor.
return( Friend );
})( jQuery );
Not too much going on here. The Friend class can take a name and a pet property. And, while it can take a pet as a property, there's nothing in the class definition that requires the Pet class to be loaded and known about before this class. This is why "friend.js" and "pet.js" were not separated by a wait() call.
The CatLover class is a sub-class of the Friend class that freaks out if a non-cat Pet is associated with it. Since it is a sub-class of Friend, it depends on Friend. And, since it uses the Pet class in its decision logic, it depends on Pet. As such, the CatLover class was separated by the Friend class and the Pet class by a wait() method call.
cat-lover.js
// Define the CatLover class - extends Friend.
window.CatLover = (function( $, Friend, Pet ){
// I am the class constructor.
function CatLover( name ){
// Call the super constructor.
Friend.call( this, name );
}
// Extend the Friend class.
CatLover.prototype = Object.create( Friend.prototype );
// Override the the setPet() to make sure that the type is a
// Cat - d'uh; I mean come on... cat's are amazing. If you're
// not a cat lover, in some sense, you're just fooling yourself.
CatLover.prototype.setPet = function( pet ){
// Make sure the pet is the good kind.
if (pet.getType() !== Pet.CAT){
// WTF?!
throw( new Error( "InsufficientPetAwesomeness" ) );
}
// This Pet is the right kind. Pass off to super class.
return(
Friend.prototype.setPet.call( this, pet )
);
};
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Return the constructor.
return( CatLover );
})( jQuery, window.Friend, window.Pet );
As you can see, this class sub-classes the Friend class and wraps additional logic around the setPet() method call.
The Pet class itself is pretty minimal. Like the Friend class, it only depends on the jQuery library.
pet.js
// Define the Pet class.
window.Pet = (function( $ ){
// I am an internal counter for ID.
var instanceID = 0;
// I am the class constructor.
function Pet( type, name ){
// Get the instance ID.
this._id = ++instanceID;
// Store the type.
this._type = type;
// Store the name value.
this._name = name;
}
// Define some constants.
Pet.CAT = 1;
Pet.DOG = 2;
Pet.BIRD = 3;
Pet.FISH = 4;
Pet.RODENT = 5;
// Define the prototype.
Pet.prototype = {
// I return the id.
getID: function(){
return( this._id );
},
// I return the name.
getName: function(){
return( this._name );
},
// I return the current type.
getType: function(){
return( this._type );
},
// I set the new name.
setName: function( name ){
// Store the new value.
this._name = name;
// Return this object reference for method chaining.
return( this );
}
};
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Return the constructor.
return( Pet );
})( jQuery );
LAB.js seems pretty powerful and easy to use. While I am not yet familiar with all the details, it seems like something I should probably start using. Since most of my JavaScript relies on the DOM-ready event, I don't often have mid-page scripts that rely on Scripts loaded in the header; as such, I can easily see my existing web applications benefiting from this asynchronous loader.
Want to use code from this post? Check out the license.
Reader Comments
@Ben,
When you said "the script tags can block other assets from loading, delay the DOM-ready event, and cause unnecessary overhead", it reminded me of something I'd read in the Nicholas Zakas book "High Performance JavaScript": Script loads share (and block) the same thread as the browser's UI. Kinda major implications follow off of that one observation.
When you think about it, that's either pretty great or pretty awful. Pretty great that you can rely on your script having its intended results, or pretty awful if your pages load slowly.
You were saying that you were feeling fuzzy about the impacts of what blocks what. The Zakas book really explains a lot. Its abundance of performance bar graphs also convey that he REALLY knows what he's talking about. Highly recommended, and available on the iBookstore.
That said, let me repeat my assertion that these nifty emerging technologies are all a big conspiracy to get us to spend all our money on tech books, so that we'll never actually get rich doing this stuff.
P.S.: Thanks again for all your pro bono work on this blog.
I'll definitely vouch for LABjs. I've been using it on a couple of websites & haven't found many issues. You have to be aware of how you are using inline scripts (if any). Anything inline JS that uses document.write() will execute before LABjs loads (as well as slow the page rendering down.)
I wouldn't recommend it for CF programmers that are dependent upon Adobe-generated javascript (ie, CFForm, CFTextarea, etc).
It's handy when including dependent javascript resources from third-party domains (ie, CDN) when you don't want them to load before your core libraries (ie, jQUery) have loaded.
Some JS libraries don't work properly when called via the LABjs loader and will have to be loaded separately. Scripts that I've had to do this with currently include:
- Google Maps API
- Sound Manager 2
- CKEditor
- a jQuery Embedly script
- SWFObject2
@WebManWalking,
I've definitely heard good things about that book. I'll have to check it out. I rather enjoy iBooks! I find reading on the iPad very enjoyable.
@James,
It's funny you bring up "document.write()". In Kyle's presentation, he basically said that document.write() was one of the worst things that JavaScript ever brought to the browser. That is should simply be removed from the language. That said, the inline scripts do take some management; but, I think of it like using the DOM-ready handler in jQuery - you just have to know that it's there and when to use it.
I hadn't thought of remote APIs like Google Maps. I suppose those might have to be loaded in the standard way. Thanks for the heads-up.
Google maps API actually has a callback parameter that can be used for asynchronously loading.
http://maps.googleapis.com/maps/api/js?sensor=false&callback=yourGlobalCallbackFn
I haven't used LAB.js so I don't know how to leverage the callback param to make it work but I've written a custom js library that extends Google Maps API with lazy loaded modules and it can be a great way to put together a js heavy app.
Calling Google Maps API without the callback doesn't work asynchronously because by default the bootstrap request pulls the rest of the library with document.write which doesn't work after page load.
I think you can figure out under which category you'd put Cara Hartmann pretty quickly:
http://www.youtube.com/watch?v=mTTwcCVajAc
Awesome stuff, Ben. I'm trying to figure out how you might use this with HTML5. Would you want to load it up Asynch the first time, then every time after that load it from the local db, synchronously? Maybe someone who's dealt w/ HTML5 more can chime in.
...and does this only work on scripts or could you potentially use this for images (sprites?)? I'm trying to think about other speed enhancements besides JavaScript loading.
Seems like you could really throw a LOT to the client, have most of your code in memory, and then -- well, basically be an iPhone app that merely performs an HTTP request only to get more data (versus HTML). Do you follow? Have your "index" page load the old fashioned way, but in the background load up many other pages/resources as you're waiting for user input.
@Randall,
If, by HTML5 and "load it from the local db", you mean the offline web app manifest, the browser will only go to that if the browser is offline from the Internet.
WMW, I was thinking more about the key/value database in HTML5, but I researched that and it's limited to...well...key/value pairs and 5 MB at that (pardon if any of that is bad information).
I thought HTML5 had something like a Google Gears / Adobe Air spec in it, but I believe I was wrong.
In short, I should have not said anything about HTML5 as it does not appear to support the functionality I thought it did.
@Randall,
Well, in that case, you might be able to say
The definition of window.f in the else could instead be the building of a script tag to get it from a server.
Might work. Your mileage may vary.
Just from taking a gander at your code, it was a bit scary everything modified the global. Have you looked at RequireJS?
With RequireJS every file is at most one module, and no file isn't a module (or shouldn't be). A module is in the most simple since a file containing a single define function. The script loader will download this file then choose to execute it, nothing executes by default. This way modules load when they are needed and no more than that.
This is mostly consistent with CommonJS modules and the require module format of NodeJS.
You definitely should check it out, I spent a few weeks analyzing them and have had no regrets using RequireJS (aka dojo.require).
@Bob,
I've only played with the Google Maps API and handful of times; but, I think depending on how you load it, you can also supply the callback inline in the code (if you use the Google code loader module or something). Certainly, an asynchronous script loader will require a shift in the way you think about loading code.
@Randall,
Ha ha, that's actually who I was thinking about when I wrote this post :)
As far as wanting to cache the scripts in the local storage, I think you'd want to keep loading them from the remote server. The reason for that is that you can allow the browser to use standard caching headers when loading the scripts.
In his presentation, one of the benefits Kyle Simpson pointed out was that since we are still loading the scripts with individual HTTP requests, the browser caching is still file specific. So, we could "invalidate" one JS file while keeping all the other files cached properly. This way, the user only has to download new code and never download redundant code.
@Drew,
To be fair, I am explicitly altering the global scope (window object) with my class definitions. I could have chosen to put those in some sort of namespace. All to say, that's not a byproduct of the LAB.js approach, but rather my use of variables.
I have looked into RequireJS a bit. It seems pretty awesome as well. And, since I have played around with Node.js, I have to say I like the term "require", although it is almost completely unrelated :) I'll probably look into that next. Now that I have some sample code, it should be easy to port it over to that.
My last post may have been a bit confusing. I wasn't intending the API url to be a link but an example of the script src. That source can be used with a dynamic script tag insert - something like:
I've written a map module in a library I've written for work and when the map module is called from the server ( asynchronously or not ) it loads google.maps as one of it's dependencies before it executes.
A simple naming convention makes loading a whole library of modules asynchronously fairly easy. I think YUI does this. We do it at work. The library unminified in total is probably several hundred k. But minified and lazy loaded a module at a time as needed it is usually less than 10k at a time. Once you accept the fact that when working asynchronously everything has to be done in a callback then you can start to identify how the code can be segmented.
@Bob,
Ah, very cool. I don't think I knew that Google Maps allows for that kind of callback. Almost like a JSONP response... sort of, ok not really :) The only kind I've worked with is an explicit callback on the API loader. I like that it can defined a bit more easily in the script URL.
Your exactly right. Really it is JSONP. That is if you don't restrict your definition of JSONP to
I tend to think of JSONP and script tag insertion almost as one in the same. Consider the following hypothetical insert:
JSONP? I'd say so. It might be a funny example but inserting scripts is definitely more flexible than callback( /*entire response*/ );
Google Maps API from what I can tell is exclusively JSONP / script insertion. If you visit a site with a google map and check the console in your browser you can see the API inserts scripts constantly. I don't think they uses a single xhr request probably the biggest reason being cross site restrictions but another advantage to JSONP / script insertion is it is faster than xhr.