public
Last active

  • Download Gist
gistfile1.js
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
// HOWTO: load LABjs itself dynamically!
// inline this code in your page to load LABjs itself dynamically, if you're so inclined.
 
(function (global, oDOC, handler) {
var head = oDOC.head || oDOC.getElementsByTagName("head");
 
function LABjsLoaded() {
// do cool stuff with $LAB here
}
 
// loading code borrowed directly from LABjs itself
setTimeout(function () {
if ("item" in head) { // check if ref is still a live node list
if (!head[0]) { // append_to node not yet ready
setTimeout(arguments.callee, 25);
return;
}
head = head[0]; // reassign from live node list ref to pure node ref -- avoids nasty IE bug where changes to DOM invalidate live node lists
}
var scriptElem = oDOC.createElement("script"),
scriptdone = false;
scriptElem.onload = scriptElem.onreadystatechange = function () {
if ((scriptElem.readyState && scriptElem.readyState !== "complete" && scriptElem.readyState !== "loaded") || scriptdone) {
return false;
}
scriptElem.onload = scriptElem.onreadystatechange = null;
scriptdone = true;
LABjsLoaded();
};
scriptElem.src = "/path/to/LAB.js";
head.insertBefore(scriptElem, head.firstChild);
}, 0);
 
// required: shim for FF <= 3.5 not having document.readyState
if (oDOC.readyState == null && oDOC.addEventListener) {
oDOC.readyState = "loading";
oDOC.addEventListener("DOMContentLoaded", handler = function () {
oDOC.removeEventListener("DOMContentLoaded", handler, false);
oDOC.readyState = "complete";
}, false);
}
})(window, document);
gistfile2.js
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12
// compressed more suitable for inlining
// ~640b before gzip
 
(function(g,b,d){var c=b.head||b.getElementsByTagName("head"),D="readyState",E="onreadystatechange",F="DOMContentLoaded",G="addEventListener",H=setTimeout;
function f(){
// $LAB stuff here
}
H(function(){if("item"in c){if(!c[0]){H(arguments.callee,25);return}c=c[0]}var a=b.createElement("script"),e=false;a.onload=a[E]=function(){if((a[D]&&a[D]!=="complete"&&a[D]!=="loaded")||e){return false}a.onload=a[E]=null;e=true;f()};
 
a.src="/path/to/LAB.js";
 
c.insertBefore(a,c.firstChild)},0);if(b[D]==null&&b[G]){b[D]="loading";b[G](F,d=function(){b.removeEventListener(F,d,false);b[D]="complete"},false)}})(this,document);

Some things to note:
1. The size of the minified snippet is ~640b before gzip. This means you will be increasing the size of your HTML page by at least this much. That's not particularly a problem, but it is something to be aware of in terms of tradeoffs.
2. My best guidance would probably be to use the snippet to load LAB.js dynamically on your first page load (landing page, etc), and then on all subsequent pages in your site, just load LAB.js normally with a <script> tag (since it should then load nearly instantly from cache).

You might want to consider using var head = document.head || document.getElementsByTagName("head")[0]; instead of what you have right now on line 5.

What if the document doesn't have a head - still valid and will still work in all browsers.... but this won't. I think its better to take google's approach and insert before the first script tag.

afaik and have tested, this works fine because all documents have an implied head even if not explicitly stated. I tested this quite a bit in various document types in LABjs and never had any failures, and this code is borrowed directly from there.

But, I'm happy to be proved wrong if you can find a place where this fails.

But when does the browser determine where the the implied document head occurs - compared to when it knows that a script tag exists? hmmm... I don't know the answer to that... But I would imagine it instantly knows where the first script is, because you're already in it (or just left it) when the document is being processed and the function is being run. If there were a multitude of scripts on a page, it would have to wait till it found something that looked like a body element before it determined the boundary between head and body.

Dude, I could be way off here as well....

It's possible that may be true. I didn't find it to be so (or at least measureable) but then again, I wasn't looking for that specific detail. I'd have to defer to other experts on that. However, my understanding was that if it encountered any content that should be in the head (or body) and is not, it immediately injected an implied head.

btw.... don't create documents without a head. :) or, rather, use LABjs on valid documents that have a head. :)

+1 on giving LABjs some <head> ;)

updated per mathias suggestions.

Why would Mathias's suggestion be better? Your original option looked fine and seemed faster on slow browsers where it made it to the second condition...

Also, I work for an ad company. So we often put our code on people's pages that we don't control. One of our publishers not having a <head> tag is the least of my worries. We do safely inject some 'third party' scripts into a page, and are considering using labJS to load an analytical testing suite. Hence, the concern.

@dave -- i think the improvement is only to use the "document.head" property if it's available (in modern browsers), and only fall back to getElementsByTagName for the older browsers. it should be available immediately if it's present, so I don't see how this would delay anything.

@getify By the way, you can omit line 22 if you want. text/javascript is the default script type in every browser. (This is also why HTML5 made the type attribute optional on <script> elements.)

juandopazo- good link, thanks. looks pretty overwhelming that a head is getting auto-created in almost every browser. that seems to lend even more support to the idea of relying on the head, either by document.head or via getElementsByTagName("head"). It also underscores the fact that you should always specify a head in your documents, even if most browsers seem to inject it for you.

Actually I read it the other way around. Event if it is a small subset, some browsers fail. Considering the whole idea of this library is to load scripts, it should work everywhere. So, as mentioned before, this seems like the best approach:

    document.getElementsByTagName("script")[0].parentNode.appendChild(myScript);

There will always be a script tag because at least this code lives inside it.

@juandopazo -- but if i have a <script> block at the end of my <body> that makes such calls to load a dozen scripts... i personally would prefer (for neatness of the DOM) if those script elements defaulted to going into the <head> rather than cluttering up the end of the <body>. that's the primary reason i'd default (and indeed LABjs does too) to putting dynamically loaded scripts into the <head>.

to be sure, LABjs itself lets you pick if you want to append to "head" or "body". but, i definitely didn't add that feature with the intention of having people use LABjs in a document with no <head> by just switching to "body".

that link indicates the very few browsers which don't auto-inject a head, not the ones which don't support head. so i'd take that to support my second statement from the previous comment more emphatically: don't make a document without an explicit <head> and expect to use script loaders properly in such a document.

Well, yes, unless someone wants to use LABjs for creating widgets inside someone else's website for example.

The problem with accumulating a lot of scripts in the body can be solved by removing the scripts elements after a couple of seconds. The scripts continue running even without the HTML tag. This is also a good practice recommended by Google (according to them, too many script nodes can cause the page to lose responsiveness, I lost this link :( ).

It can be dangerous to have a generalized loader that assumes things like the ability to remove a script element from the DOM after it's been "used", that is why LABjs does not do that. The danger is that some scripts have behavior in them where either immediately, or sometimes much later, they will query the DOM looking for their own parent script tag, to get data attributes, or inspect the URL used to load the script, etc. A general loader like LABjs cannot assume there's any safe time which it can remove such an element, because it might break such logic.

There's of course nothing stopping you as a user of LABjs in your widget module loading from removing the script elements later, since you know that those script elements don't rely on that behavior.

Moreover, I'm not sure I understand why being able to have all the script elements for a module load at exactly the same place in the DOM where the widget is located helps in cases where you're trying to have a low impact on the existing page? If your module creates 5 script elements, whether they're in the head, end of body, or right next to/before your widget's <div>, there's still 5 script elements. It's the exact same impact to memory usage, etc.

I understand that people may need to "clean up" after the script loader if there's a lot being loaded. But I don't think that their ability to do that will at all be affected by where those script elements are added to the DOM.

Yeah, I meant that keeping a lot of script nodes hurt performance wherever they were placed.
I didn't consider the weird cases of people using script tag attributes or innerHTML. Very good point.

Thanks for the well thought answers and great job on your library!

Doesn't using a loader script to load a loader script seem a bit odd to anyone? The full LABjs has a lot of features... has anyone thought of designing a loader that has minimal features in it by default (and is, say, 640 bytes, like this snippet), and if you need more features, the extra features are simply loaded modularly with the minimal loader before use? That way people aren't wasting bandwidth and time with features they don't need, and you don't have code duplication like when you have a loader to load a loader...

yes, dburry, it seems quite odd. I don't personally do this myself on any sites. I think LABjs being so small 2.2k gzip'd makes it pretty irrational to be concerned about that small amount of code being loaded with a normal blocking script tag (can't possibly take more than 100-125ms tops on average). some people are ultra-concerned about every millisecond, and so there's a chance (perhaps in the mobile space more than desktop browsers) that this bootstrapper for LABjs might be useful.

As for my sites, I personally create a file that I call load.js, and inside that file I inline the minified source for LABjs, and then I have my $LAB chain(s) listed for loading all my other script resources. That way, in my HTML, I only need one script tag to load "load.js", and that file is <3k, and it does all the rest of my script loading exactly as I need. I personally feel that having that $LAB chain code hidden away inside load.js is better than having it in inline script blocks in my HTML, mostly because it takes advantage of caching that 300-400 bytes of code (along with LABjs's code).

As for creating a smaller and more modular loader... again, at 2.2k, I don't think the size of LABjs is big enough to justify the overhead (and complication in code) of creating a conditional modular system. Even jquery.js core, at 25k, has a bunch of separate modules, but almost everyone serves the full jquery.js "combination" file, not loading individual pieces and taking up a lot of HTTP overhead. So, if that's the overwhelmingly common pattern for larger libs like jQuery, how much more true should "single file only" be for a resource so small as LABjs?!?

What would you be doing, anyway? Conditionally loading 2-4 files that are each 300-600 bytes in size? It's a bad precedent to be loading lots of extra files anyway, so I'd never ever suggest having more than one file for my script loader, under any circumstances. This is exactly why I keep LABjs so small. Sure, it has "features" to it, but it's nowhere near as complex as other loaders that can do fancy things like CSS loading and so forth. It keeps itself laser-focused only on loading JS, and nothing else.

I can understand the argument of 2.2k or even 25k being "small enough" and "not worth the effort" to make either one more modular. Obviously it hasn't been worth the effort yet to me either, since I haven't written such a modular system myself yet!

But I still think I'd use it gladly if someone did it, perhaps contribute to such a project, especially if anyone else had interest too. The recent rise in popularity of delayed and parallel script loading techniques opens up a whole new range of possibilities of being much more modular, and potentially reaping a lot of benefits from it without suffering so much "overhead" from it... Every little bit of savings does add up at some point, a K here and 2K there, pretty soon you've saved 20K or 50K or even more.

And with more and more cores in modern CPUs and execution parallelism possible even too (web workers), the possibilities for real performance increases are extending again on another front. I am obviously not speaking just of LABjs but of much larger popular libraries like jQuery as well, of course. Where could all this lead in the end? Someday perhaps even a javascript execution environment that more closely resembles an "operating system" than a "script" or "library"...

@dburry -- I still think that even if there was a "modular" loader, the overhead of having to load several 400byte modules would far outweigh any benefit you got from your initial loader being 640 bytes.

So, you might say, "what if I never need a loader to do those fancy things?" Then, I say to you, write your own inline loader code (similar to my snippet above), and don't use anyone's loader. And btw, there are script loaders which are much smaller than LABjs. They don't cover all the same use-cases, and aren't as cross-browser robust. They don't give you the same configuration capabilities. But if none of that is important to you, by all means, use them. :)

If on the other hand, you have cases where you sometimes need a really small loader and sometimes need a more robust one (like LABjs), what's the real harm in using LABjs for all those cases? Sometimes it's a little bigger (maybe 1k max?) than you needed, but that's so small a difference it's likely to wash out anyway.

But the benefits are shared caching, as well as (intangible) benefit of having more consistent code across your projects/pages. I've found that the simpler and more consistent my code is, the more likely it is to be "correct" and "performant". The more variations you entertain, the more likely something is to slip through the cracks and come back to bite you.

My intention isn't to argue so hard that 2.2k should be more modular.... 2.2k is already very small, and the benefits of chopping it up further are therefore logically also probably small. My intention is to try to inspire people in general to think more modularly... Script loaders like LABjs can make the "overhead" of such modularity less of a problem.

I'm sorry that I opened this dialog with poking fun at a "loader to load a loader" and suggesting modularity to solve that... that seems to have sidetracked the main point I'm ultimately wanting to make :(

So my main point is: Please, everyone, use LABjs and other such script loader techniques to make your OTHER scripts (i.e. the ones that LABjs is loading) more modular! It can help save you a lot! LABjs is great, keep up the good work! And let's use it in the way that benefits us the most... with more modular code.

Secondary to this main point.... If anyone out there wants to pinch every byte and thinks LABjs itself is too big, and wants to use this "loader to load a loader" in this gist... then perhaps they should consider writing a more modular loader, since in theory that could be better than having duplication between the smaller and larger loaders.... Again, the benefits are probably very small, yes, since LABjs itself is so small, but we don't really know by how much for sure until we try it and measure it (maybe even no measurable benefit at all). Not worth it enough to bother even trying? well, yes, obviously that's been the case so far, since nobody's done it yet...

@dburry -- gotcha. perfectly valid points to be making. thanks. :)

Some really small loaders to consider:

Does anyone know what "item" in head means?

("item" in head) is an expression which asks if head has a property called "item", which is testing to see if head is an actual DOM node or still just a placeholder DOMNodeList entry.

Ah awesome, thank you.

One more question... what would happen if you added scriptElem.async = true into the mix?

pretty much nothing at this point. dynamic script elements have, for all intents and purposes, acted like "async=true" all along. The only exception being FF 3.6, where their behavior differed some. But starting in FF4, as well as upcoming Webkit (and hopefully Opera and IE soon), scripts will default to showing their property as "true" by default (and behaving like it).

So, the only real value to changing an async property value on a dynamic script element will be that you can change it from the default "true" set it to "false", which will cause it (and other scripts with async set to false) to still load in parallel, but to execute in correct (insertion) order (kinda like regular non-async script tags do in markup).

I didn't realize that -- very interesting. Thanks again for the help!

Wouldnt it be possible to make this snippet smaller/simpler?

Say i define a function such as LABjsLoaded() using this type of code
var LABjsLoaded = function(){
// cool $lab stuff here
};

Then load LABjs using something smaller like google analytics style snippet

(function() {
  var labjs = document.createElement('script');
  lab.src = '/path/to/lab.js';
  lab.setAttribute('async', 'true');
  document.documentElement.firstChild.appendChild(lab);
})();

The only difference would be that the base labjs file does a LABjsLoaded() after loading. (205 bytes before gzip)
Would be nice if labjs itself looked for the existence of a particular function, if it exists, call it once labjs is ready.

@sajal-
I am not a fan of that pattern/suggestion:

1) it promotes having a global function not on the LABjs namespace, which is a bad practice to pollute the global namespace

2) it would hardwire that the name of the function had to be exactly something as manually specificed in the LABjs source code, which makes it brittle if someone can't define a global function with that name for whatever reason (conflicts, etc).

3) also, i still feel it's necessary to have the the document.readyState part of the snippet in there.

All in all, I think this snippet is pretty small as it is. And loading LAB.js itself dynamically is of questionable benefit anyway, because LAB.js is already itself so small, that a normal blocking script element load of it shouldn't cause any kind of noticeable delay.

Honestly, I recommend to people that instead of loading LABjs dynamically like this snippet does, instead to inline LABjs' source code into another file that they'd already be loading onto their page... For instance, on all my sites, what I do is have a single "load.js" file that I load at the bottom of the body, with a regular blocking script tag. Inside that "load.js" file, I have LABjs source code minified inline, and then I have my other page initialization code... for instance, I have my $LAB chains for what other scripts I want to load (like jQuery, etc). By having that all in one file, and loading it with a normal blocking script tag, it becomes basically like a "bootstrapper" for the page. As long as "load.js" stays decently small (<5-8k), it works pretty well.

@getify: aha come to think of it it does make sense now to use the regular <script> tag for labjs.

Ive already working on a mechanism to bundle all self owned js including labjs into a single file, and all 3rd party js is handled by $LAB . No reason to not use <script> ... nothing more to block anyways...

I was getting a little too carried over trying to over-optimize.

You probably mean: var head = oDOC.head || oDOC.getElementsByTagName("head")[0];

@tobie -- no, check lines 13-19 for how it does that

@tobie -- it's to address an older IE quirk/bug where the [0] element is not yet set, and if you capture a ref to it too early, when it's made available, your ref will be invalid.

Heh. Deserves a comment, don't you think?

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.