This has certainly been an eventful day so far. I woke up this morning to this ominous tweet from @3rdEden. I thought surely I was reading that wrong, but I scrambled to try and install a copy of FF4 nightly (aka “minefield”) to try and confirm. Sadly, a few minutes later, I confirmed that it was in fact true: FF was no longer preserving execution order (as insertion order) on dynamically injected <script> nodes.
Then I thought to myself: surely this is just a regression bug… FF has always preserved execution order on <script> tags (hint: so has Opera). Also, the recent support for “async” and “defer” properties was specifically designed around unpinning scripts from execution order and onload dependency blocking, so the natural assumption is that the desired and correct behavior for a “naked” <script> tag without such attributes would be to continue as it always has in FF: insertion-order execution.
So I figured I’d just file a bug with Mozilla and surely they’d just fix that little hiccup. Then the bombshell… I found the bug report that changes everything. In short, Mozilla intentionally removed support for preserving insertion-order execution of inserted scripts. Not an accidental regression bug in a nightly release, but a landmark and fundamental feature change they’ve made to the browser. And no, they didn’t engage anyone like me (or others that are highly invested in this area) in any discussions ahead of time to examine the impact such a change might have — they just made the change. Bam.
Why do we care?
Let’s back up. Why do we care that Mozilla is changing this behavior with the upcoming FF4? The reason I care, and you should too, is because it severely cripples script loaders (like LABjs). If you’re not aware, the affected script loading tricks/tactics are in several different loaders (including the “Order” plugin for RequireJS), and LABjs is in use on a number of high profile sites, including the New Twitter, Zappos, and Vimeo.
Before I go any further, let me explain exactly under what circumstances and how this change by Mozilla is going to change things for the script loader landscape, especially for LABjs.
The specific use case that is affected is: dynamic script loading of multiple scripts (one or more of which are from remote domains) which have execution order dependencies so their order must be preserved.
If you are only loading local scripts, or if you are loading scripts in parallel and you don’t care at all about their execution order (they are unrelated entirely), or if you are only loading one script, then you are probably not affected. So move along. Well, unless you use another site that does, and that site will now fail in FF4. Then I guess you should care.
Why is this relevant to LABjs?
So, let me try to explain very simple boiled down why LABjs relied on the previous behavior in FF, and thus what the effect will now be in FF4 going forward.
LABjs’ main goal is to allow you to load any script, from any location (local or remote), in parallel (for better performance), but maintain/enforce execution order if you need to because of dependencies. What does this mean? It means that LABjs needs to be able to download all scripts in parallel, but be in control of when/how those scripts execute. Because one of the later scripts might download quicker than the earlier scripts… but we need the later script to “wait” for the earlier script to finish downloading and run.
When you use <script> tags in your HTML, you already get this correct behavior by default. But LABjs is designed to replace those <script> tags with $LAB API calls that load the scripts dynamically and in parallel, which achieves (in some cases, much) better performance than just <script> tags alone.
Unfortunately, not all browsers work the same way with respect to the loading and executing of such <script> tags. Prior to today, FF and Opera operated like we want, which is that you can inject as many <script> tags as you want, and the browser would make sure they download in parallel in whatever order, but will make sure they execute in proper (insertion) order. This is ideal and desired behavior from a script-loader (and performance) point of view. But the other browsers didn’t play so nicely, so this technique was only reliable in FF and Opera.
In IE, Safari, and Chrome, if you inject two <script> tags dynamically, they may not execute in insertion order. Which means that if we want to download them in parallel but control their order, we have to find some other trick to do that. So, for those 3 browser families, I devised the “text/cache” preloading trick.
Put simply, in those 3 browser families, you can inject a <script> tag into the page with a fake `type` attribute of something like “text/cache” (or “foo/bar”, etc). Those browsers will download that script, and will fire the `onload` handler to let you know the script finished loading, but they will not execute the script since the type is not recognized. So, the “preloading” trick for these browsers is to download all scripts in parallel (caching them in the browser cache) using this fake type attribute, and then go back and re-insert new <script> tags in the proper desired execution order, using the correct “text/script” type. The script will be recalled from cache immediately and executed, and then the next script can immediately be processed the same way, and so on.
Is that trick pretty? Absolutely not. But it works. It works really well, by exploiting parallel downloading for sometimes HUGE performance gains. And yet it keeps a key feature of LABjs: preserving execution order if dependencies require it.
The problem is, this preloading “text/cache” trick does not work in FF or Opera! They will refuse to download a resource if the declared type of that resource is not something they recognize ahead of time. But take heart, because FF and Opera preserve execution order, so for those browser families, we just insert the <script> tags all at once, and rely on the browser to maintain the execution order.
So, that’s the uneasy but workable tension that LABjs has operated under for the last year and a half. It uses one trick for some browsers, and another behavior for other browsers, and between those two tricks, “preloading” (parallel loading) and execution order enforcement are possible. LABjs hides all the ugly details of how to do that under the covers and keeps the usage pretty simple and straightforward.
This balance of performance and dependency preservation was never (to my knowledge) achieved in a script loader before LABjs cross-browser, and is a big reason why LABjs is now powering many big sites like Twitter — performance and features are the ideal combination.
The fly in the ointment
By Mozilla deciding that FF will no longer respect/enforce insertion execution order, they have now killed the only “trick” which worked for FF family of browsers to be able to parallel load dynamic <script> tags but ensure execution order for dependencies. It’s important to note that <script> tags that appear in plain HTML source, even in the new FF4 world, do exhibit the desired behavior — what’s changed is that dynamically inserted <script> tags no longer do. BIG bummer.
What are the options for LABjs at this point?
- The most likely, but severely distasteful, option is that LABjs can be patched to detect that it is running inside of FF4 (only), and it can simply disable all “preloading”. What this will mean is that sites relying on LABjs running in FF4 will load AND execute completely serially (that is, one script at a time). This means that each of 3 scripts on a site will load one at a time and then execute, then the next script downloaded, executed, then… You can see why the performance side of this equation is going to lose out BIG TIME in that respect. LABjs will still work, but all of the benefits performance-wise will be completely lost.
In fact, LABjs in FF4 will actually run LESS PERFORMANT than if the site just used regular script tags. Of course, sites will probably still want to keep using LABjs to speed up browsing for visitors in all other browsers (including especially IE’s), but they’ll have to face the sad fact that their FF4 visitors will see a greatly reduced performance profile. Pretty sad for such a cutting edge browser to actually cause much worse performance.
- I could patch LABjs to, in FF4+ only, use cross-domain XHR to “preload” remote scripts. However, the problem this presents is that the way FF has implemented cross-domain XHR, it requires the remote server to opt in with special response headers, which means that ALL servers, especially CDN’s, will have to be patched to start serving such headers, before the cross-domain XHR option will be viable. That’s not going to happen any time soon, even if we start now, and it’s certainly not going to happen before FF4 releases final.
- We can petition Mozilla to reconsider and restore this behavior to FF4 before its launch. I call on the community to help me in this respect.
In fact, I’ll assert that I strongly feel that Mozilla owes more responsible behavior to the community — they should have engaged us in a discussion of such features before making such a fundamental and backwards-incompatible change.A serious dialog needs to be had regarding all the key players and compromises be made that will not leave script loaders (and the many big sites who rely on them) out in the cold. Edit: I apologize if the above language is inflammatory or offensive. I’m obviously distressed. And I wish I’d known about this so I could have helped inform Mozilla on the larger side-effects of the change as implemented.
- All browsers need to sit down and come up with and agree on a consistent behavior for the dynamic loading of resources (especially .js files) that takes performance concerns as well as dependency enforcement into account. If they all agreed on a single standard, LABjs (and other script loaders) could be drastically simplified or almost made entirely moot. This would be the best option. Script loaders like LABjs only exist right now because the browsers haven’t yet agreed on a consistent and performance-savvy way to handle such functionality. I hope one day they are unnecessary, but that’s going to require a lot of uncommon agreement/cooperation between the browsers.
I have petitioned for such discussions to happen for well over a year. There’ve been a few emails exchanged among a few players in this space, but nothing has even begun to be dealt with to finally solve these issues.
I hereby renew my call for actual standards to specifically deal with how resources are dynamically loaded, including all adjacent functionality like execution order, onload event notifications, etc, and for ALL browsers to immediately agree to such standards and to publish backward patches.
Until that happens, performance-oriented script loaders like LABjs will still be a necessary evil in this web world, and we’ll continue to be at risk of browser nightly releases casually removing key features/behavior and throwing the whole script loader world into chaos.
I of course will keep you, the community, aware of the status of this issue as it moves forward. I sincerely hope that Mozilla will reconsider this situation and will engage in discussions to find a solution. I ask you, the community, to help me surface to Mozilla why that is such a critical conversation that needs to happen ASAP.