Thoughts on script loaders
Last week, Steve Souders released his ControlJS project. The goal of the project is to give developers more control over how and when JavaScript files are loaded and executed on a page. It does so by using Stoyan Stefanov’s approach of preloading JavaScript without executing it and has the pleasant side effect of enabling parallel downloads. For more details on usage, take a look at Steve’s three blog posts.
The first blog post contains some criticisms in the comments from Kyle Simpson, the creator of LABjs, another script loader. LABjs’s goal is a bit different than ControlJS: to enable parallel downloading of JavaScript files while maintaining execution order. To do so, LABjs needs to know which browsers allow parallel downloads by default and then provide other solutions for the browsers that don’t.
Both LABjs and ControlJS have a major problem: they’re using various browser detection techniques to determine the correct course of action to optimize script loading. Some have argued that LABjs’s browser inference is safer than ControlJS’s user-agent detection, but I disagree. Browser inference is feature detection plus assumptions and is an inherently flawed approach (seriously). Browser inference isn’t more accurate than user-agent detection, nor is it less likely to fail. I’m not saying that user-agent detection is a great thing, but at least it’s explicit in what it’s trying to detect. I choose explicit over implicit every time as it helps to prevent errors or, if errors occur, identify them faster. But this is a debate that’s tangential to the point of this post.
LABjs has already proven that this approach, browser-based forking of script loading techniques, is a bad idea. It’s just too fragile to withstand the onslaught of browser updates that we’ve been seeing, which is why I’ve never suggested using script loaders that try to outsmart the browser. Kyle faced a serious issue when Firefox 4 nightlies started showing up that broke the behavior of LABjs. The issue was that dynamically inserted script elements were no longer guaranteeing execution order, which was something LABjs relied on. The change was made to bring Firefox in alignment with the HTML5 spec and other browsers. ControlJS will undoubtedly run into the same issue as browsers continue to evolve. Maintenance of such solutions comes at a high price.
The real problem(s)
There has been some debate over what the real problem that LABjs and ControlJS are trying to solve. In truth, there are three problems represented by the libraries.
First, both are trying to enable parallel downloading of JavaScript resources. That’s a worthy goal but one that’s already being handled by newer browsers. Though it’s an academically interesting pursuit to try to squeeze out parallelization of JavaScript downloads in older browsers, I don’t believe it’s practically worthwhile. Browsers are already solving this problem for us, so script loaders aren’t needed to help there.
Second, LABjs is very focused on maintaining script execution order. With this comes an assumption: that you want to download multiple JavaScript files that have dependencies on one another. This is something I don’t recommend but I recognize that some people feel it’s important. ControlJS is not concerned with this. Either way, this is a problem that is not being handled in a rational way by browsers so if you want this behavior, you must use a script loader.
Third, ControlJS is very focused on separation of download and execution of JavaScript. Built into it is the idea that you should be able to download a JavaScript file and not execute it until a point in time determined by you. It’s an interesting concept and one that’s been through a lot of experimentation in the community (as Steve points out in his blog post). The assumption here is that your page is progressively enhanced such that JavaScript isn’t immediately needed. LABjs doesn’t address this problem. Browsers are also not helping with this.
A call to arms
Though Kyle and I have differences of opinion on many things, I think he said it exactly right when he called for a common solution to problem #2. We shouldn’t need script loaders. There should be native ways to achieve all of the things developers need and want to do with JavaScript files. The script loaders have showed us the ways in which developers are trying to solve performance problems, and the logical next step is to have the browser vendors internalize these and come up with ways to solve them. Kyle put together a lengthy examination of the issues and proposals for how to address problem #2 (note: no one has come up with a proposal to solve problem #3). I’ll admit, Kyle asked for my feedback as this was going on, but I was very wrapped up in a few projects and didn’t have time to really dig in until now.
async=false?
A proposal introduced by Kyle calls for a strange augmentation to the async attribute of <script> tags. The async attribute is a Boolean attribute, meaning that its very presence indicates the feature should be turned on, which also means that the attribute value is of no consequence. So the following three lines are equivalent:
<script async src="foo.js"></script>
<script async="true" src="foo.js"></script>
<script async="false" src="foo.js"></script>
These act as HTML5 specifies: they begin to download immediately and execute as soon as they’re finished downloading without preserving order. In JavaScript, you can enable or disable this functionality by setting the corresponding async property on a script element:
var script = document.screateElement("script");
script.async = true; //enable async per HTML
Under Kyle’s proposal, setting the async property on a script element using JavaScript would trigger a new mode. So the meaning of this code has changed:
var script = document.screateElement("script");
script.async = false; //switch into new mode (WebKit nightly, Firefox 4)
Previously, setting async to false would have no effect. Now, setting async to false in supporting browsers makes the scripts download in a non-blocking manner while maintaining execution order.
While I applaud Kyle’s tenacity in pushing through to a proposal, I’m a bit baffled by this. To me, this code reads as “this script is not asynchronous” instead of “this script is asynchronous and please preserve the order.” Once again, I favor explicit over implicit to avoid errors.
An alternate proposal mentioned in his twiki is to create a <scriptgroup> element that logically groups script files together:
<scriptGroup id="group1" ordered="true">
<script src="foo.js"></script>
<script src="bar.js"></script>
<script>
somethingInline();
</script>
</scriptGroup>
I actually like this proposal a lot. It’s explicit, there’s very little doubt as to what is going on here, and you could conceivably attach an event handler to the <scriptgroup> element that could tell you when all files have been loaded. It does introduce another element, but in the interest of clarity, I think this overhead is validated by the obviousness of the developer’s intent.
Separate download and execution
There is still no good, consistent solution for separating download and execution of JavaScript, something that I think is very necessary. This isn’t just for the initial loading of script files on page load, but also for the dynamic addition of new code after the page is loaded. In my presentation, Performance on the Yahoo! Homepage, I spoke about how we trickle in JavaScript after the page is loaded so that it’s ready when the user makes another action. The ability to preload JavaScript and execute later is absolutely becoming more important, and that’s really the problem that ControlJS is trying to tackle.
In an ideal world, I’d be able to do something along the lines of this:
var script = document.createElement("script");
script.type = "text/cache";
script.src = "foo.js";
script.onload = function(){
//script has been loaded but not executed
};
document.body.insertBefore(script, document.body.firstChild);
//at some point later
script.execute();
That’s all I want. I don’t want to make a request to download a file and then make another request expecting that the file is in cache – that’s a very fragile solution to this problem. What I want is to download the file, have it sitting in cache, and then later just call a method to run that code. This is what ControlJS is modeling.
In the end
Both LABjs and ControlJS are attempting to solve the JavaScript loading problems in different ways. Kyle and Steve are both smart guys, pursuing their approaches for solving similar and slightly different problems. The good news is that we now have two script loaders that show the various ways developers are trying to load scripts on their pages, and hopefully that’s enough to get the browser vendors to come together and agree on longer-term native solutions so we won’t need script loaders in the future.
In the short-term, with apologies to both Kyle and Steve, I can’t recommend using either. While both illustrate interesting approaches to script loading, the reliance on browser detection means that they will require constant monitoring and updating as new browser versions come out. Maintenance is important in large web applications and these libraries presently add maintenance overhead that isn’t necessary.
I know this is a heated topic lately, so I’ll ask everyone to please try and keep your comments civil.
Update (22 Dec 2010): Changed description of how async=false works, as my original example incorrectly showed functionality working with markup when in fact it works only with script.
Disclaimer: Any viewpoints and opinions expressed in this article are those of Nicholas C. Zakas and do not, in any way, reflect those of my employer, my colleagues, Wrox Publishing, O'Reilly Publishing, or anyone else. I speak only for myself, not for them.
Both comments and pings are currently closed.




31 Comments
My gut feeling was to avoid maintenance overhead by avoiding loaders. Now that you’ve clarified the details, i’m no longer worried about not having the time to research this more. Cheers.
Mario Awad on December 21st, 2010 at 9:18 am
Interesting post, Nicholas. It’s good to get a third person’s view on this. I also read Kyle’s post about ControlJS, and he disagrees with some of Steve’s decisions. Logically, if Steve had completely agreed with everything that LabJS did, he wouldn’t have built ControlJS. Therefore having someone writing a post from the fence, so to speak, is great.
I won’t get into the semantics of user agent sniffing vs. browser inference, but it’s obvious that both approaches are too dependent on what browser vendors do. However, I agree with you that I prefer clarity.
That said, we’ve been using LabJS for the past year or so and it performs very nicely for us. As long as Kyle can keep it patched for whatever new browsers come up with, we can keep using it until it’s no longer needed. The same goes for ControlJS.
Ron Derksen on December 21st, 2010 at 12:06 pm
Great write up as usual, Nicholas. ControlJS is definitely a short term solution until browsers support the desired behavior, and it is going to require upkeep. In the near future LINK REL=”PREFETCH” would be a simpler solution – we just need more browsers to support it but at least the spec is already done. Another technique to evaluate is the ability in IE to create a script element but not add it to the DOM. This causes the download to happen but padding and execution is delayed until the script is inserted into the DOM. This is a slight variation on your pattern, aapendChild is simply moved to the “later” part and executeScript isn’t needed.
I agree with your long term wishes, but I also want a way developers can get better performance until the specs and browsers catch up.
Steve Souders on December 21st, 2010 at 12:52 pm
Maybe people should just do like some others do:
don’t use frameworks, just copy the ideas in a small framework appropriate for the site, just the part actually use/need and then you have less code.
And all these problems go away.
I mean it, if you can’t combine your javascript in just 1 or 2 downloads and put those script-tags at the end of the page. Then maybe you are doing it wrong.
I just checked loading the new Twitter is 3 js-files, Youtube is just 1 core file and 1 per template.
For everything else, use the optimizations provided by the browser. You can use feature detection, maybe only load a selector API when the browser doesn’t have a selector API*
Kind of like progressive enhanchment.
When a browser doesn’t have the optimizations, maybe these people will start to notice a problem and get a new browser. Otherwise we are helping them stay with the old browsers.
I don’t agree with everything he says/does, but I’m starting to think maybe Andy Clark has some good points (he was talking about style though):
http://hardboiledwebdesign.com/talk/
* I know the browser API is limited, but when you use complicated selectors it is slow anyway and supposedly you don’t like slow.
Lennie on December 21st, 2010 at 12:59 pm
Nice read. I would really be interested in your thoughts about a multipart xhr script loader.
https://github.com/jAndreas/Supply
The basic technique for this to work comes right of your book. The performance is pretty stunning.
Andreas Goebel on December 21st, 2010 at 1:21 pm
The more I think about it, why not load only parts of the javascript code based on the capabilities of the browser. You can do:
if (hasCapabilities) {
// load website.js
} else {
// load websiteLite.js
}
Even Youtube loads a different page based on what network you are on so it will load quick for everyone:
http://velocity.oreilly.com.cn/ppts/Front-endPerformanceImprovementsatYouTube.pdf
http://v.youku.com/v_playlist/f5333814o1p11.html
Lennie on December 21st, 2010 at 1:33 pm
Nicholas-
I appreciate your post and your thoughts. I have a several thoughts (as usual):
1. LABjs currently uses a direct feature-detection for async=false. I know you don’t like async=false (I’ll address in a moment why I think you misunderstand the intent), but at least it’s feature-detectable, which is a huge part of what I’ve been pushing for. None of the other loaders, and none of the other hacks involved in this space, even come close to a feature-detection.
On that note, I think LABjs is forward thinking, and deserves more credit for standing out in future-aware coding, credit which I don’t think you or others give it.
Secondly, every time I’ve ever spoken or written about the hacks & inferences inside of LABjs, I’ve done so saying “these are ugly hacks, and need to be removed once we get browsers to support the use-case better”. And I’ve been actively advocating for it in various ways for quite awhile. We’re finally starting to see some movement by spec and the browsers to recognize the use-case (#2 specifically) as valid.
But again, I don’t feel like the larger community around the resource loaders (with the exception of James Burke & RequireJS) has helped much in pushing the browsers or the spec on this issue. So, while the hacks are ugly, at least I (through LABjs) have been trying to get those hacks phased out, in favor of direct, sensible, feature-testable support. Other loaders and their authors have been rather silent/absent from that discussion.
I hope more than anything that *this* changes soon.
2. The #1 use case above (enable parallel loading)… it’s not just about parallel loading of scripts between each other. The MAJOR POINT you’re missing is that dynamic script loading makes script loading parallel to the page’s resources. In other words, it unblocks the page’s DOM-ready *AND* it unblocks other resources (images, css) from loading. This is absolutely crucial, and is by far the biggest advantage to a script loader.
3. Actually, LABjs is *not* in the business of trying to preserve order by default. If you do `$LAB.script(…).script(…).script(…).wait(…)` All three of the scripts specified there will, by default, load asynchronously and execute as soon as possible. It’s only when you start injecting .wait() clauses into the middle of your chain in between `script()` calls that you activate LABjs’ order-preservation.
Obviously, you understand that things like jQuery and jQuery-UI are always going to be reality on the web, and there will always be cases where one script has to execute before another. We may not like that reality, but it’s reality nonetheless. LABjs just accepts that reality and provides an easy way to do it, if you need it. But the performance-savvy default is “as soon as possible” (at least in browsers where that’s possible).
4. async=false — first of all, make sure you understand that the spec right now defines “async=true” *and* “async=false” (the default), but ONLY for script tags in the HTML markup (aka, “parser-inserted”). Secondly, the spec-defined behavior of dynamically inserted (aka, “script-inserted”) script elements is practically identical to “async=true” parser-inserted script elements.
So, the *only* part of this puzzle not well defined in the spec is to be able to dynamically insert a script element and get it to behave like a normal script element (aka “async=false”). By “behave”, I mean that it maintains execution order, like normal parser-inserted (async=false) scripts do.
My reasoning is, if we want scripts to maintain execution order, we basically want them to have the behavior that is currently only available if the script tag was in the markup, with an assumed default value for `async` of false. So, the proposal is to give dynamically inserted elements the ability to opt-in to the same behavior (order preservation) that normal markup script tags get by default.
I don’t understand how that’s not completely sensible and symmetric, with little impact on existing spec and functionality? Have async=true and async=false well-defined for BOTH parser-inserted scripts and script-inserted scripts.
Now, I *hate* the attribute name “async”. I think it’s completely off-target. But they chose it, and there’s no going back on that. All I am doing with my proposal is to suggest that the spec should be fully specific and consistent on all 4 cases, not only the 3 that it currently specifies.
Bottom line: 2 major browsers (Firefox and Webkit) have *implemented* async=false as proposed. That leaves Opera and IE still to convince. But at this point, my proposal is much further along in getting traction with browsers and the W3C than any of the alternatives.
5. I like <scriptgroup>, too (it was partly my idea). It’s a lot more flexible than `async=false`. But it didn’t get much traction in the discussions with the W3C. It’s also a much more radical change than async=false, and thus will take a LOT more time and discussion with browser vendors and the spec committee before it’ll ever see the light of day.
Maybe it will get more traction someday? Maybe if you and others join in and advocate for it?
6. As Steve said, there actually IS already a behavior implemented in IE (since v4 in fact) which helps exactly the use case of #3 in your post. And the best part is, it *does not* rely on the ugly assumption of script cacheability, like the <object> and `new Image()` hacks do.
Not only has IE had it implemented forever, but it’s also *in the spec*. It’s just that none of the other browsers have implemented it yet. Perhaps they will get around to it someday, but it’s obviously important that we as a community advocate to get them to do so sooner, if this #3 use-case is important to us.
Here’s the thread I started on that topic:
http://lists.w3.org/Archives/Public/public-html/2010Dec/0177.html
Ironically, this exact same mechanism, if the other browsers would implement it, would also solve #2, and better and more flexible than both `async=false` *and* <scriptgroup>. But again, it’s been in IE for a LONG, LONG time, and in the spec for quite awhile, and no other browsers have embraced it yet. So, it may still be quite awhile before we can rely on it.
I *can* say this: LABjs’ next release (shortly) will include a *feature-test* to select this behavior, as its preferable in IE to the “cache-preload” hacks. Again, I believe LABjs is leading the way in getting us to a non-hack, feature-testable solution to use-cases #1-3 (and others).
I hope others will now join that cause.
Kyle Simpson on December 21st, 2010 at 1:38 pm
Great article as always. Thanks! I like the idea of the scriptgroup tag. This is slightly off the topic, but if we are proposing changes to the script tags, might I propose changes for better caching? For example a lot of script loading today is of common libraries like jQuery and YUI. These libraries are used on many sites, yet many sites host them themselves, so the browser can not use a version of the library cached from other sites. I propose adding attributes to the script tag like this:
script src=”jquery.min.js” name=”jquery” version=”1.4.4″ md5=”276acf5f04c4c60aaf73e66177dc43f7″ type=”text/javascript”
I think it is self explanatory. If those optional attributes are specified the browser will use a cached copy with same name and version (assuming both are provided). It will only load the script if the MD5 checksum matches. Modern browsers might even come preloaded with common javascript libraries.
One more suggestion, backup sources. For example:
backup=”"http://code.jquery.com/jquery-1.4.4.min.js;https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js;http://ajax.microsoft.com/ajax/jquery/jquery-1.4.4.min.js”
If the src fails to download or fails to match the checksum, it will try the backup sources in turn until it succeeds. It can also use them to check the cache further improving your chances of loading from the cache.
Adam on December 21st, 2010 at 1:58 pm
Hello Nicholas,
This isn’t a two-horse race. There are more than two loaders out there. Have you looked any of the other script loaders?
RequireJS http://requirejs.org/
StealJS http://jupiterjs.com/news/stealjs-script-manager
Backdraft Loader http://bdframework.org/docs/loader/loader.html
Head.js http://headjs.com/
There’s also yepnope.js, an extension to LABjs.
What is very interesting about RequireJS and Backdraft is that they allow the developer to eliminate the “script execution order” problem by following the CommonJS AMD proposal (http://wiki.commonjs.org/wiki/Modules/AsynchronousDefinition) to specify asynchronously-loaded script and resource (CSS, HTML, IMG) dependencies.
StealJS also indicated that they’re interested in following the AMD proposal: http://twitter.com/#!/javascriptmvc/status/12626676584288256
This approach offers a fine-grain level of control you’ll never, ever have with HTML-based solutions like the proposed element and async attribute.
If anything, I think you should be promoting the solutions that support the proposed JavaScript standards (and also avoid the issues you’re concerned about).
Regards,
– John
unscriptable on December 21st, 2010 at 2:21 pm
@Lennie – YUI 3 now does capability-based loading.
@Steve – Thanks for the additional info, I honestly did not realize that IE had that behavior. I’d still favor something more explicit, but nice to know it’s there.
@Kyle – I only brought up browser inference vs. user-agent detection because I felt this was an unfair criticism of ControlJS over LabJS, and that was not specifically targeted at you but rather others who were pointing this out. As I said, completely tangential.
As I said, I just think async=false is incongruous with the actual meaning. async=false is not the opposite of async=true…this has effectively become a three-state attribute instead of a two-state one.
I’m curious if you reached out to the WHAT-WG with the proposal? I don’t believe you have, but I tend to get better responses from them than the W3C.
@Unscriptable – I wasn’t intending to cover all script loaders in this post. Kyle and Steve were the ones who asked my opinion on the state of things, and so I wanted to respond to them. Thanks for the extra resources, though, always useful to see more examples.
Nicholas C. Zakas on December 21st, 2010 at 4:13 pm
@Nicholas-
I appreciate your post, and your feedback in the comments thread. I think you are definitely helping to spur much needed discourse on these topics.
You and I have had many disagreements (friendly of course) over UA sniffing vs. browser inference. I won’t rehash that whole thing yet again.
But I do think it’s inaccurate to imply that UA Sniffing and Browser Inference are basically the “same” in terms of drawbacks. It’s also inaccurate to suggest that LABjs’ problems were caused because it was browser-inferencing (as if UA sniffing would have been better). The problem (the lack of proper loading support in the browser) would have affected LABjs either way.
The thing I most take issue with is the suggestion that LABjs is “the same type of wrong” as ControlJS. It’s a fact (that everyone awknowledges) that to reliably do the advanced script loading we’re trying to do, we *have* to browser fork. How we do so is either UA sniffing or browser inferences, cut and dry. How we pick the “lesser of two evils” is of course up for debate.
But, I think what’s missing is that LABjs deserves credit for leading the way toward eventually not having these hacky techniques at all:
1) LABjs is using (and preferring) new feature-tests over the ugly inferences, as they become available; other loaders like ControlJS are not (yet?).
2) I, through LABjs, am directly part of the effort to get the spec and browsers to give us these features natively, in feature-testable ways; other loaders (and their authors) are not (yet?) participating in that process and are instead blindly writing more and more code based on the hacks, which makes the problems harder to eventually solve, because of fear of breaking “existing legacy content”.
——
How is `async=false` creating a 3-state script property? I don’t follow you on that one.
——
I absolutely did reach out to (more than one member of) the WHAT-WG group on more than one occasion. At the time (much earlier this year), I was met with strong resistance to the idea of even needing to standardize these things, let alone what my ideas for standardizing them might have looked like.
That effort died down for awhile, and I pursued other avenues of advocacy, such as writing and speaking. It wasn’t until October that I re-engaged (forced to!) the standards process, this time the W3C at the suggestion of Henri (from Mozilla). And later in October, I set up the current proposal home page on the WHAT-WG wiki, at the suggestion of Tantek Çelik (also from Mozilla).
http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order
Kyle Simpson on December 21st, 2010 at 5:08 pm
@Kyle – I think you just argued my point for me. I do acknowledge that you need to do browser forking in order to handle this type of script loading – I know of no other alternative (and I gave Steve this feedback directly as well). My point is that because this is the only way to do it, the implementations are inherently fragile. I also know that everyone would use feature detection if it were possible, and I’m naturally happy when things move in that direction.
The async attribute, as currently defined in HTML5, is a boolean attribute. That means it’s a two-state attribute, either present or not and the attribute value actually has zero effect. I could put async=”foo” async=”bar” async=”nicholas” and it all means exactly the same thing (effectively async=true). In this world, even async=”false” means the same as async=”true”. So the mere presence of async means that the script starts to download immediately while not blocking other scripts or items on the page. Leaving off the async attribute would then be the equivalent of async=false, which means that the script behaves as it always had.
With your changes, the three states are no async attribute (default behavior, download in parallel, keep ordering, and block), async=true, which behaves as described in HTML5 presently (download in parallel, don’t keep ordering, don’t block), and async=false, which behaves like async=true except that execution order is preserved (download in parallel, keep ordering, and don’t block).
With regards to the WHAT-WG, I’d highly recommend (for future reference) bypassing individuals and heading straight to the mailing list.
In any event, glad I can bring some more attention to the topic.
Nicholas C. Zakas on December 21st, 2010 at 8:21 pm
Completely agree with your post. Scripts loaders seem largely to be solving a problem caused by third-party scripts and interdependencies. Hopefully we’ll trend back to smaller scripts and fewer scripts in the near future as developers target the new browser versions.
I would think a browser would load a script async whenever it could and then execute as inline, async, or deferred as it was supposed to.
Keith on December 21st, 2010 at 8:33 pm
@Nicholas –
Thanks for the clarification on the async “three state” perspective. I now think I understand what’s wrong.
Here’s where I think the breakdown communication wise has been. Frankly, you bring up a very good point about an important difference to the `async` attribute. But it shouldn’t result in a “three state” situation, so I’ll address how that can/should be.
The communication problem has been the difference between how a script element’s *attribute* works versus its reflective *property* in JavaScript. When a “boolean” attribute (ie, present or not present) is reflected into a JavaScript property of the same name, it always takes on a `true` or `false` value. And my entire perspective on my proposal was from the JavaScript property side of the equation (hence the true/false semantics).
I never really intended to affect the behavior (or definition) of parser-inserted scripts and their `async` attribute (which as you correctly note, is currently a present/not-present boolean attribute). My proposal was only to put some sense into the consistency/symmetry of the definition of the dynamically created script element and its `async` *property*.
To illustrate, consider this test: http://test.getify.com/test-script-async.html
In pretty much all browsers, here’s what you get:
markup script without `async`: false
markup script with `async` = false: true
markup script with `async` = true: true
dynamic script’s `async`: false
First, notice that a parser-inserted script element without `async` present still ends up reflecting as a *property* set to false. And conversely, when async is present, a property is reflected with value true.
Moreover, a dynamic script element’s reflective `async` value just happens to be false, but it’s actually not spec’d or defined to be anything, at the moment.
In modern browsers like FF 3.5, Chrome, etc, which implement script-async, a *script-inserted* dynamic script element behaves almost exactly like a parser-inserted script with async attribute turned on. That last line of output is what troubled me, because a dynamic script element *behaves* like async=true, but it reports false in the reflected property. The first thing the proposal did was change that to true to be more accurate in its reflective value.
Secondly, for a script-inserted script element, I asked that you be able to set the `async` *property* value to false, to get that script element to behave very similar to (with respect to script ordering) to a parser-inserted script element that had no async present (which would in fact reflect itself in a property to be =false).
I assumed (incorrectly) that if, in markup, I did async=”false”, it would be the same as no `async` at all. That’s because I didn’t realize that the `async` attribute was this special boolean present/not-present, rather than explicit value setting.
In fact, though I wasn’t explicit in my proposal thus far (because I was confused until now), my proposal actually should say that the `async` attribute (and its reflective property) need to CHANGE from being this special boolean present/not-present to being an attribute that either has a “false” value (same as it not being present) or a truthy value (like “true” or “async” or “foobar”).
If you run the above test in FF4 nightly (aka FF 4b8 when it releases in a few days), you’ll see this output:
markup script without `async`: false
markup script with `async` = false: true
markup script with `async` = true: true
dynamic script’s `async`: true
Notice that the dynamic script `async` now == true (like it should, I think). But what’s missing (and confusing) from this implementation, as you correctly helped me see, is that setting the markup script attribute to “false” should in fact deactivate `async` (just like async not being present) and it should reflect in its *property* as false.
To be clear, the only falsey value for the `async` attribute should be “false”. Any other value should be truthy and result in “true” behavior.
I need to go back to Firefox (and Webkit) and advocate for them to make this further change to bring it fully into consistency between the attribute and reflective property values. But once that is addressed, I think my `async=false` proposal is pretty rational.
Kyle Simpson on December 21st, 2010 at 10:05 pm
On fixing the inconsistency:
Mozilla bug: https://bugzilla.mozilla.org/show_bug.cgi?id=620852
Webkit bug comment: https://bugs.webkit.org/show_bug.cgi?id=50115#c37
Kyle Simpson on December 21st, 2010 at 10:57 pm
Excellent analysis which, as usual, leads to thoughtful debate!
A few years ago I accidentally discovered IE’s unique pre-fetching behavior which, at the time, I found completely foolish. Only recently, when script pre-fetching becoming the topic du`jour, did I realize that this very behavior, coupled with its readyState implementation (which I also deemed foolish at the time), were in fact the solution to these problems. This page demonstrates execution order can be managed without sacrificing parallel downloading.
Similar in implementation to XHR script loading, it also cleanly separates the download and execution. In the ControlJS menu example, the click event could trigger immediate insertion of the existing script, even if it’s in-flight. This seems simpler than having to wait for pre-fetching to complete, hoping the cache is populated (which may or may not be the case) and only then creating and inserting the SCRIPT. (It also works around caching quirks )
Beyond the semantics of the async=false paradigm, the approach is lacking in one critical way: It only provides a single synchronous execution queue. Event-driven enhancement only works if you can have a reasonable degree of certainty the code will run “soon enough” and not be forced to wait inline behind third-party libraries not required to satisfy the user’s request. The readyState model solves this problem.
Expressing the relationship B.js needs A.js is simple using plain-old HTML and we need a way to do this dynamically:
But what if what B.js actually need is A-1.js a dynamically inserted by A.js? Prototype.js (at least older versions) is a good example of this. By itself, prototype.js is useless: it’s sole purpose is to load its own modules via document.write() which, to my knowledge, is the only way to accomplish this kind of sub-module loading.
Will Alexander on December 21st, 2010 at 11:28 pm
Firefox 4 lets you opt into ordered execution of document.createElement-created scripts by setting the async DOM property to false (magically defaults to true for nodes created with document.createElement). Firefox 4 downloads in parallel scripts that occur in the document’s source and executes them in order (unless marked async or defer). Firefox 4 also downloads document.written scripts in parallel and executes them in order (unless marked async or defer).
See http://hsivonen.iki.fi/script-execution/ for the long version.
You misunderstood the .async=false thing. Assigning false (the JavaScript boolean literal, no quotes) to the async DOM property opts into ordering in Firefox nightlies and beta 8. Setting the value of the async markup attribute to “false” means the exact opposite, because “boolean attributes” in HTML work based on the presence of the markup attribute–not based on the value of the attribute. Likelise <input disabled=”false”> disables an input rather than enabling it.
I’d really appreciate if you could edit the post to strike over the bogus example. Otherwise we’ll see a large number of Web developers who read your blog because of your book fame copy and paste async=”false” thinking it means “not async” when it actually means the opposite.
As for the scriptgroup thing, it would have been a complex new feature of a completely different magnitude and we needed a fix that could be put into Firefox 4 after the feature freeze and that had a good backout strategy.
Quoting Steve:
Unfortunately, on the Web short-term solutions always become long-term ones. People who deploy ControlJS today will have their sites littered with UA sniffing after you deem the short-term need to be over.
Henri Sivonen on December 22nd, 2010 at 6:54 am
+1 to Henri’s request to update the blog post. My proposal (and FF4 and Webkit’s implementation of it) never had ANYTHING to do with setting the `async` attribute on a markup script tag to “false”. It was always about the `async` property on dynamically created script elements being set to `true` or `false`.
However, as I’ve asserted here, and on that bug I filed with Mozilla last nite, I *do* think that setting async=”false” in markup should do something. Just not what Nicholas said. I think async=”false” in markup as an attribute should be exactly identical to not setting an `async` atttribute at all.
Kyle Simpson on December 22nd, 2010 at 11:05 am
Confused by the whole
asyncattribute proposal. Can we keep things consistant and on spec, please?<script id="foo" async src="foo.js"></script><script id="foo" async=async src="foo.js"></script>
<script id="foo" async="async" src="foo.js"></script>
should be identical and mean that
asynchas been set totrue. This must be reflected in the DOM API by the following:var script = document.getElementById('foo');script.async;
// -> true
script.hasAttribute('async');
// -> true
On the other hand, if the async attribute is missing:
<script id="foo" src="foo.js"></script>it must be reflected in the DOM API by:
var script = document.getElementById('foo');script.async;
// -> false
script.hasAttribute('async');
// -> false
For the record, the spec is pretty explicit about boolean attributes:
Tobie Langel on December 22nd, 2010 at 12:52 pm
@Henri & Kyle – I’ve updated the description, thanks for the clarification. I still find this implementation confusing for the reasons already stated.
@Henri – I completely understand that you wanted to get a stopgap solution into Firefox 4, I just think that there could have been a more explicit solution. For instance, perhaps adding an
orderedattribute to a script element. It seems like order execution is a separate but related concern fromasync, as it could also affect thedefercase.Nicholas C. Zakas on December 22nd, 2010 at 1:19 pm
@Tobie-
While you are correct about how the JavaScript property SHOULD reflect a value of `true` or `false` indicating the presence or absence of the `async` attribute in the markup, my whole point here is, if I dynamically create a script element (not in markup), everything is confusingly different.
Firstly, a dynamic script element *behaves* basically almost identical to a markup script tag with `async` attribute turned on. But the `async` property of that dynamic script element *doesn’t* show `true` like it should.
Moreover, the more important point you’re missing is, for a dynamic script element, if I *set* the `async` property (to true or to false), currently, that behavior does nothing. So, I can turn async on or off in markup with the `async` attribute, but I cannot turn it on or off with the value of the `async` property. That asymmetry/inconsisency is what’s troubling.
And THIS is what my proposal fixes.
Kyle Simpson on December 22nd, 2010 at 1:36 pm
@Nicholas –
I appreciate your update of the blog post. It’s a lot more accurate. But there’s still a problem with what you say.
> var script = document.screateElement(“script”);
> script.async = true; //enable async per HTML
This is not true. you currently cannot “make” a dynamic script “async” or not by changing the `async` *property* in JavaScript. The `async` property is always set to `false` for dynamic script elements (should be `true` to mirror behavior), and changing it does nothing.
———
Also, regarding “ordered”… the W3C comment thread discussed that option indepth. It was felt that it created more confusion than helped. I’d encourage you to read the whole W3C thread and also the wiki, if you’re interested in seeing how the proposal evolved.
Kyle Simpson on December 22nd, 2010 at 1:43 pm
@Kyle – That’s the way it’s documented, but as far as I know, Firefox 3.6 behaves in the way that I described. This was discussed at length on this post: http://www.nczonline.net/blog/2010/08/10/what-is-a-non-blocking-script/
Nicholas C. Zakas on December 22nd, 2010 at 2:12 pm
Here are a few test pages that demonstrate some of these concepts and concerns:
* IE parallel-loading, ordered-execution
* pre-fetching double-loads
* readystate solution to double downloads
I am personally of the opinion IE’s readyState model is a more robust solution than the `async=true` which simply recreates the flaws inherent to the current FF behavior but makes it optional to do so. IMO, it, doesn’t make sense for the spec to require that ordered execution require assuming the risk unrelated scripts block that execution altogether. In many situations, the benefit of parallel-loading is not enough to assume this risk. I believe readyState seems to provide a nice solution to this issue and evidence of its applicability can be seen in the number of loaders that use XHR loading whenever possible.
IE’s “anonymous” src attribute fetching addresses this issue but, as Kyle points out, is considered optional by the spec. IE’s model allows the script to be added to the document anytime, regardless of its current readystate, and executed ASAP. This simplifies delayed execution implementation. In addition, the readyState property could be inspected and a “Please wait” message shown if the script is not all ready loaded.
If I can add a fourth point to the List of Problems it would be that we need a way to emulate the parallel loading and preserved execution order provided by “parser-inserted” scripts dynamically. For example, older versions of prototype would load modules dynamically. While it used document.write() to accomplish this (and newer versions have abandoned this technique) it seems this is a fairly common and useful technique and not one that is covered by the async proposal (or adoption of IE’s readystate for that matter)
Will Alexander on December 22nd, 2010 at 3:50 pm
@Kyle — This page tests the async=true attribute on script-inserted scripts and Nicholas’ assertion seems correct: http://digital-fulcrum.com/controljs/t/ffasync/
Will Alexander on December 22nd, 2010 at 4:24 pm
@Nicholas — I’m guessing FF 3.6 implemented that as a temporary stop-gap because their continued preservation of insertion-order execution on dynamic script tags was in willful violation of the spec, which clearly states (and has for awhile) they should execte “as soon as possible”.
Moreover, FF4 (b8) now adheres to the spec with respect to default behavior (aka, defaults async=true), and additionally (per my proposal) allows you to access the other (currently non-spec’d) insertion-order execution behavior by setting it to `false`.
The way you word that section in *this* post (yes, you’re more clear in the previous one, which is now obviously out of date a little bit) makes it sound like setting `async=true` is just obviously “how you do it”.
> In JavaScript, you can enable or disable this functionality by setting the corresponding async property on a script element
In fact, that’s only a temporary hack that FF 3.6 allowed, and isn’t in spec (yet). I’d suggest in your post above that you at least indicate that setting it to true only does something in FF 3.6, and that it now defaults to the opposite in FF 4 as of b8.
Moreover, it’s all confusing because you follow that section (where you say “see, this is how you do it, you set it to true or false”) with the next section saying you disagree with my proposal to allow “setting it to false” to do something meaningful. Which is it? Do you agree with setting the property to true or false for such behavior control, or not?
In fact, in light of FF3.6, my proposal simply continues (but flips the default, per spec) what FF 3.6 already implemented (that setting both true/false *do* something important), behavior which you’ve endorsed in two posts now.
So why again do you disagree with my proposal, since it seems to fit pretty well with what FF 3.6 does?
Kyle Simpson on December 23rd, 2010 at 2:55 pm
[...] Dalton) – takes a look at various ways to measure JavaScript performance accurately Thoughts on script loaders (Nicholas Zakas) – Zakas gives his opinion on some of the issues new script loaders should be solving and some [...]
JavaScript Magazine Blog for JSMag » Blog Archive » Apple iAd is PastryKit 2.0, Game engines, Google Body Browser on December 24th, 2010 at 6:54 pm
The problem with script loaders is that they are not that useful at least when compared with module loaders. The key advantage of module loaders (such as RequireJS) is the module dependency management with async resolution.
Take a look at Ext JS. They have a really nice widget library, but they don’t use proper dependency management. As a result, the Ext code is very monolithic and Ext apps are less scalable.
Les on December 26th, 2010 at 11:25 am
[...] 126. Thoughts on Script Loaders [...]
RegexHacks :: Blog » The Top 150 Web Development Highlights from 2010 on December 31st, 2010 at 4:45 am
Thanks.
That’s actually what I proposed initially. See the HTML WG archives for the discussion that lead to making async magic instead. (I now think that making async magic was indeed a better solution than making the async DOM property have no effect on script-created scripts and having an ordered DOM property with no effect or corresponding markup attribute on parser-inserted scripts.)
Henri Sivonen on January 3rd, 2011 at 8:47 am
[...] 126. Thoughts on Script Loaders [...]
The Top 150 Web Development Highlights from 2010 « Web Development « blog.officinazerouno.it on January 7th, 2011 at 9:02 am
Comments are automatically closed after 14 days.