Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with:

Using The LAB.js Script Loader With jQuery's DOM-Ready Event

By Ben Nadel on

As I talked about last week, LAB.js is an asynchronous JavaScript loader that loads and executes remote scripts in an extremely efficient, non-blocking manner. With that efficiency, however, comes the complexity of working outside the "normal" top-down page flow of the document. This means that the DOM-Ready event provided by jQuery has to be integrated in to the LAB.js load/wait event life-cycle.


 
 
 

 
  
 
 
 

When you use LAB.js to load your remote JavaScript files, the DOM-ready event is likely to fire a lot faster than normal. This is because the asynchronous loading of the remote JavaScript files no longer delays the DOM events. However, just because the DOM is ready sooner than normal doesn't mean you can just execute JavaScript code at the bottom of your page (as you would in a traditional "blocking" approach). Since the remote JavaScript files are loading asynchronously to the page itself, you now have to ensure that both the scripts and the DOM are ready to be used before you try to manipulate any markup.

Since the JavaScript files are being loaded asynchronously, I like to initiate their loading in the HEAD of the page. This way, LAB.js can immediately start loading them in parallel with the rest of the DOM. As with the traditional model, I then like to put my "init" scripts at the bottom of the page. In order to make sure the init scripts fire after both the DOM-ready event and the script loading, I need to put my init scripts in the wait() callback of the asynchronous script loader. In order to do so, I have to keep a reference to the "chainable queue" responsible for loading my JavaScript files.

To see what I'm talking about, let's take a look at a little demo. In the following code, I'm loading the remote JavaScript files from last week's blog post. Then, at the bottom of the page, after the DOM-ready event, I attempt to use the loaded scripts:

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Using LAB.js And jQuery's DOM-Ready Event</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. All scripts depend on jQuery. Then,
  • // only the cat-lover script depends on the friend.js to
  • // be loaded (so it can extend it).
  • //
  • // NOTE: We are storing a reference to the QUEUE so that
  • // we can refer to it later in the code.
  • var queue = $LAB
  • .script( "../../jquery-1.6.1.js" )
  • .wait()
  • .script( "./friend.js" )
  • .script( "./pet.js" )
  • .wait()
  • .script( "./cat-lover.js" )
  • ;
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <h1>
  • Using LAB.js And jQuery's DOM-Ready Event
  • </h1>
  •  
  •  
  • <!-- .... Page content would go here .... -->
  • <!-- .... Page content would go here .... -->
  • <!-- .... Page content would go here .... -->
  •  
  •  
  • <script type="text/javascript">
  •  
  •  
  • // I run when the DOM has loaded and all of the external
  • // scripts have been executed by LABjs.
  • function init(){
  •  
  • // Create a cat-lover.
  • var sarah = new CatLover( "Sarah" );
  •  
  • // Create a cat.
  • var mrMittens = new Pet( Pet.CAT, "Mr. Mittens" );
  •  
  • // Associate the kitty with the cat-lover.
  • sarah.setPet( mrMittens );
  •  
  • // Log a success.
  • console.log( "Oh " + sarah.getPet().getName() + "! You're so cute!" );
  •  
  • }
  •  
  •  
  • // When all the scripts are loaded, add a listener for
  • // the DOM-ready event so that we can also make sure that
  • // the page has loaded. We'll use jQuery for this (which
  • // will have already been loaded by the time this wait()
  • // method is executed).
  • queue.wait(
  • function(){
  •  
  • // Attach the DOM-ready listener.
  • jQuery( init );
  •  
  • // NOTE: Since we are already at the bottom of the
  • // page, we could have also just passed INIT to the
  • // wait() method:
  • //
  • // queue.wait( init )
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, I am using LAB.js to load the remote scripts. The LAB.js script() and wait() functions both return a reference to the chainable object used to load a group of dependent script files. I keep a reference to this chainable object so that I can then invoke the wait() method one more time at the bottom of the page in order to execute the init() script.

Since I am at the bottom of the page already, I don't technically have to use jQuery's DOM-ready event; I could just pass the init function reference to the wait() method as a callback. This would be a bit more straightforward; but, for demonstration purposes, I'm still hooking into the DOM-ready event.

When we run the above code, we get the following console output:

Oh Mr. Mittens! You're so cute!

Since I didn't actually change any of the remote script files, I won't bother showing them here. If you are interested in how they work, please take a look at last week's post.

Once you start using an asynchronous JavaScript loader like LAB.js, you have to shift the way you think about using your scripts. But, since we are already used to using callbacks in our JavaScript code, the shift is fairly minimal. Rather than assuming a script has already loaded, you simply have to add an additional callback to the script-loader. And, as you can see above, this additional step is extremely easy to work with.




Reader Comments

Nice screencast. What I got from it is that LABjs puts you in control of script loading, instead of just doing what the browser does based on markup, which isn't optimal.

I just pulled up the MDN docs for the script tag:

https://developer.mozilla.org/En/HTML/Element/Script

Interesting, but I'm glad that I can use something like LABjs and not worry about where to add the async attribute.

Once again another great post on LABjs. I am in the process of doing a big project where we are using LABjs and doing more or less what you have posted. In my code we actually need to load other scripts based on what DOM nodes are available. So, if we go off your init function we are doing something like:

  • function init(){
  • var checkboxes = jQuery( 'input[type=checkbox]' );
  • queue.script(
  • function(){
  • if( checkboxes.size() ){
  • return 'jquery.ui.checkbox.js';
  • }
  • }
  • )
  • .wait(
  • function(){
  • checkboxes.checkbox();
  • }
  • );
  • }

Again, great write up.

Have you tried this instead regardless the script position?

  • $LAB
  • .script( "../../jquery-1.6.1.js" )
  • .wait()
  • .script( "./friend.js" )
  • .script( "./pet.js" )
  • .wait()
  • .script( "./cat-lover.js" )
  • .wait(function () {
  • jQuery(function () {
  • // your init function
  • // triggered on DOMContentLoaded
  • // or instantly if the document
  • // is already ready
  • });
  • })
  • ;

I think Andrea's point is correct (though from twitter conversations I think he may be misunderstanding Ben's reasons on this page).

1. You can split the $LAB chain (as Ben does here), or not (as Andrea does)... shouldn't be any different. Definitely doesn't matter with respect to DOM-ready -- same behavior either way.

2. There are *other* reasons besides anything to do with DOM-ready that you may want to split a chain like this. For instance, you may have a CMS system that combines different template sections together in different parts of the page, and so you may have part of your chain defined in one section, and another part defined in another section. If that's the case, or there's some other reason, then splitting the chain as Ben has done is perfectly fine.

OTOH, if there's no real reason to split the chain, it's unnecessary complication, and you should just include it altogether in the head.

:)

Kyle agreed on cases where split the chain may be necessary, but being this post title "Using The LAB.js Script Loader With jQuery's DOM-Ready Event" I do believe there is a misunderstanding about LAB.js patch that works already with jQuery.ready() regardless the DOMContentLoaded has been fired before or it will be fired after.

Please Ben update/clarify the post, thanks.

@Ben A,

Yeah, exactly! LAB.js allows you to control, in a uniform, cross-browser way the order in which the scripts load / run. This will remain consistent even as the HTML spec changes and the browsers start to implement their own loading approaches.

@Benjamin,

Whoa - I just realized there are 3 "Ben"s on this post :) Awesome! High-fives all around.

That said, that looks pretty good. I like that LAB.js can accept a Function as the script argument for dynamic script loading. Cool stuff!

@Andrea,

Yeah, that's doing the same thing. Since I am storing a reference to the chain, our final "wait()" calls are the same - I just have some stuff in the middle.

@Kyle, @Andrea,

I guess my reasoning for splitting the chain was 1) just to see if it could be done, and 2) because that way, the ONLY thing in the Head was the script loading. All other character content was pushed the bottom of the page. I figured that way, there was as little pre "content" content as possible. Since I would have to wait for the DOM to be ready to interact (something that, at the bottom of the page wouldn't need to the jQuery dom-ready binding anyway), I might as well put it at the bottom.

This way, the "visible" content could load as fast as possible... then the scripts could execute later.

@Andrea I think we are saying the same thing, there reasoning behind the:

  • var checkboxes = jQuery( 'input[type=checkbox]' );
  • queue.script(
  • function(){
  • if( checkboxes.size() ){
  • return 'jquery.ui.checkbox.js';
  • }
  • }
  • )

Is that I don't want the check box code to load if there are not checkboxes on the page. In my situation I am trying to load all the "painting" files before any of the hard core code. And the reasoning I did not just do:

  • .wait()
  • .script(
  • function(){
  • var checkboxes = jQuery( 'input[type=checkbox]' );
  • if( checkboxes.size() ){
  • return 'jquery.ui.checkbox.js';
  • }
  • }
  • )

which I believe may have been your other point is that LABjs - and @kyle, correct me if I am wrong - evaluates all the scripts before everything is loaded and the jQuery call would fail (and has in my testing).

@Andrea,

I don't understand the "patch" you are referring to? Maybe I am not understanding what you are saying? Does LAB.js already have a DOM-ready event listener?

Ben, the jQuery ready events works just fine with LAB.js, this is my point.

If you use jQuery.ready event no need to check if the content is there because unless it's not injected asynchronously by other scripts, and in this case these scripts should give you a way to know when things are done, you are already sure that the content is there: DOMContentLoaded and readyState do the trick internally, no reasons to split the queue polluting the content of the page with the JavaScript logic.

The wait(function) will be executed only when it's time so in this particular case just move your code inside jQuery(function () {}), as done in my example, and keep things simple: one script tag to rule them all via LAB.js

Andrea-
Agreed that it's perhaps a little confusing here why Ben split the chain. If he did so specifically because of DOM-ready, then you're correct that this is a misunderstanding. A little clarification as to why the chain is split would be helpful.

Moreover, there is a common misconception (unfortunately reflected in this article) that putting some code at the bottom of the body is a magical cure-all for not needing the DOM-ready listener. I have found cases (older IE, mostly) where even code at the end of the body still needs to be wrapped in a DOM-ready listener. I don't think it's a good idea for people to assume position in the markup has anything to do with whether the browser is ready or not.

------

The last thing I'll mention is, I think the spirit of this post is good, because I believe strongly that people confuse/conflate DOM-ready with "scripts are done loading". When you only use script tags, that just is a happy accident that it's true, because script tags have that ugly "block DOM-ready/onload" behavior that we all hate.

All along, it's been an anti-pattern (IMHO) that people make that assumption. Certainly, once you introduce script loading, which separates the two, it becomes obvious that they (DOM-ready and script-loaded events) are in fact not related.

The script loader (in this case, LABjs) provides a callback for when "scripts are ready". You should use *that* callback for logic that needs to wait on the scripts. $(fn) in jQuery adequately provides the "DOM is ready" event, and you should use *that* listener for stuff that is DOM dependent.

In short, don't use "DOM-ready" as a proxy for "scripts are loaded", and moreover, don't use "scripts are loaded" as a proxy for "DOM-ready".

If you have script logic that is not dependent on DOM-ready, don't wait on the DOM to run it -- run it ASAP. If you have code that is not dependent on the scripts, don't wait on the scripts to finish to run it -- run it ASAP. Combine the two listeners *only* when you have logic that needs to wait on both the scripts finishing AND the DOM being ready.

@Andrea,

Are we just having a "style" conversation? Because from what it looks like, our code is "functionally" exactly the same. You are putting the jQuery DOM-ready binding inside the last wait() invocation of the loading chain. Then, you put your "init" code inline within the dom-ready binding, and I am just factoring it out into a named function.

I think our code is equivalent - we just have different style??

Benjamin-
"which I believe may have been your other point is that LABjs - and @kyle, correct me if I am wrong - evaluates all the scripts before everything is loaded and the jQuery call would fail (and has in my testing)."

Correct. In your case, because you need to use jQuery to check your condition (are checkboxes present on the page), then you have to nest your call so that you make sure the .script(fn) isn't executed until after jQuery has loaded and is available.

However, if you could somehow check for the existence of checkboxes without needing to use jQuery, then your .script(fn) call could have been not-nested and directly off the main chain, which would have the slight benefit of letting checkboxes.js load in parallel with the rest of the chain.

@Kyle,

My final wait() call is at the bottom of the DOM. This means that the wait() call will only be found once the DOM is "basically" loaded. That takes care of the dom-ready potion... basically. I only included the jQuery dom-ready binding since it would make the position of the final wait() independent of its position within the DOM. If you look at the comments in the JS, you can even see that I state that IF you put it at the bottom *inside* the final wait(), then technically, the jQuery aspect of the dom-ready isn't really necessary.

Then, the fact that it is in a wait() call takes care of the fact that the scripts are "ready," irrelevant of the DOM state.

I think maybe I am not understanding where the confusion is? What am I missing?

no Ben, it's not about the style, it's about this article that mixes up LAB.js and jQuery ready event and what Kyle said already right after me and before your last reply.

I am just saying: it's confusing, reduntant, superfluous, as it is presented right now.

Either explain properly why you decided to split or change the topic because it's misleading, imo.

My final wait() call is at the bottom of the DOM. This means that the wait() call will only be found once the DOM is "basically" loaded
nope, and this is exactly the misleading/incorrect part ... anyway, your call.

@Ben-
As I pointed out in my previous comment, I believe it's a faulty assumption to say that because the .wait() is found at the end of the body, that basically DOM-ready listener is not really necessary.

If that is true in practice, it's an indirect behavior (accident) at best. Position in the DOM is a poor indicator of "DOM before this is fully parsed and ready". Certainly in some older IE's, this bad assumption could cause fatal browser crashes in certain circumstances.

I think the cleaner, better, safer pattern to encourage devs to use is, as I said:

1. if logic needs neither the DOM nor other scripts, and can run right now, then do it. (example: XHR calls)

2. if logic needs other scripts, but does not need the DOM, then put that logic in the script loader callback.

3. if logic needs the DOM, but does not need other scripts, then put that logic in a standalone DOM-ready listener callback.

4. if logic needs both the DOM and other scripts, put a DOM-ready listener inside your script loader's callback.

@Andrea,

I guess this is the part that I don't understand. If the page is being parsed in a top-down manner, then how does the final wait() call (which is the last element in the entire page) not basically indicate that the DOM is loaded?

If I am misleading anyone, it is only because I don't understand myself? Can you explain.

Summary
If you use LAB.js and jQuery is loaded and you wait after that, you can simply use jQuery(readyFn) if the purpose is to be sure that the DOM has been loaded.

  • $LAB
  • .script( "../../jquery-1.6.1.js" )
  • .wait()
  • .script( "./friend.js" )
  • .script( "./pet.js" )
  • .wait()
  • .script( "./cat-lover.js" )
  • .wait(function () {
  • jQuery(function init() {
  • // your init function
  • });
  • })
  • ;

If I misunderstood everything, never mind, still use upper example to have a callback on DOM ready, regardless the script position which is does not grant anything on DOM level.

@Kyle,

Ahhhh, if you are saying that an inline Script tag does not dictate that the DOM before said inline script tag is not necessarily ready to be interacted with ... then yes, this is a technical misunderstanding on my part.

I have never heard of this being a problem before!!! That kind of blows my mind. I thought the inline scripts would always ensure that the DOM prior to them was loaded.

@Andrea,

I am sorry, but I think we are doing the same thing. Except mine looks like this:

  • var chain = $LAB
  • .script( "../../jquery-1.6.1.js" )
  • .wait()
  • .script( "./friend.js" )
  • .script( "./pet.js" )
  • .wait()
  • .script( "./cat-lover.js" )
  • ;
  •  
  • chain.wait(function () {
  • jQuery(function init() {
  • // your init function
  • });
  • });

The only difference I am seeing is that I am not directly chaining the last wait() call. This is a style difference, not a technical difference.

@Kyle,

Is the inline script issue (where preceding DOM cannot be referenced) documented anywhere? Is it true for any of the modern browsers? This is a really serious concern as it is an assumption that I (and I assume many people) are making!

@Ben-
Specifically in the case of older IE's, the problem is that it depends on what you did inside that inline script tag.

For IE, the fatal crash would come if you tried to modify an element that had not yet closed. So, if your script tag was inside the body, even at the bottom, and what you did was try to modify the body specifically (append something to the body), then it could crash IE (<=6 or 7, can't remember).

If however you modified some other element in the body, that itself was already closed, it was fine.

Moreover, even though browsers do top-down parsing, they also do various lookaheads and things like that, so I just think it's potentially unsafe to assume that in all cases, position implies readiness.

There's a lot of nuance there, beyond what I think most devs are capable of fully navigating. So I find the better, clearer, and safer pattern to be as I suggested in my previous comment. Doing things that way will never run into problems. Making assumptions may or may not work. I prefer determinism. :)

@Kyle,

Very interesting! I had no idea that was an issue. Granted, when I was using IE6, I was really only hooking up hover events for drop-down menus :)

That said, it seems like my misunderstanding here was that of how the DOM works, not with how LAB.js works?

However, if you could somehow check for the existence of checkboxes without needing to use jQuery, then your .script(fn) call could have been not-nested and directly off the main chain, which would have the slight benefit of letting checkboxes.js load in parallel with the rest of the chain.

@kyle: I am actually doing in cases where I can and makes sense if( document.byId(someid) ) so that there is not dependency on jQuery needing to be there.

@Ben just check the source code of this file:
http://www.3site.eu/ready.html

most likely you gonna have true first, and false after.

Why? Because a node in the dom, script included, does not guarantee that the DOM has been parsed already.

This means you may have lots of surprises such:
* not working selectors
* missing document.body
* wrong collections

something else you never know.

To be sure that the DOM content is the one expected you need a DOMContentLoaded listener and/or an onload and/or a readyState === "complete"

Every other technique may suffer problems or cause browsers parsing problems/crashes.

I hope this is enough.

@Ben-
"That said, it seems like my misunderstanding here was that of how the DOM works, not with how LAB.js works?"

I'd say that's a fair statement. I really think Andrea's objections were just that the title of the post is about DOM-ready, and then you also do this splitting thing, which implies that you're doing so *because* of DOM-ready. As we've explored, there were some subtle misunderstandings of the DOM and DOM-ready at play.

Perhaps some slight clarification in the article to explain things is in order (your call).

But as I said, I think overall, the spirit of this article was right on!

@Benjamin-
Cool. Yeah, if you could assume QSA, you could do something like:

var checkboxes;
$LAB
...
...
.script(function(){
checkboxes = document.querySelectorAll("input[type='checkbox']");
if (checkboxes) return "checkboxes.js";
})
.wait(function(){
if (checkboxes) { // checkboxes present, AND checkboxes.js was loaded
$.checkboxes(...)
});

@All,

So it seems the moral of the story is to always use the DOM-Ready stuff, even if your script is at the end of the page. I can live with that.

Now, the question becomes, should I care about for demo purposes? I try to be vigilant about "Best practices", but typically, the DOM-Ready stuff is not the primary point of the post... and, if I can remove one level of indentation, I'm happy.

But, I want to make it a habit to use. I'll try to be dedicated about it.