Executing script-inserted external scripts in insertion order

Hi,

The interoperable platform (i.e. what works across browsers without UA sniffing) doesn't provide a way to make script-inserted external script execute in the insertion order. Interop is lacking, because IE and WebKit do one thing (they execute script-inserted external scripts as soon as the scripts are available and the event loop gets to spin) and shipped Firefox and Opera do another thing (they maintain order across *all* scripts). 

On a seemingly unrelated but, in practice related, point, interop is lacking for handling of script-inserted external scripts whose type attribute contains something that doesn't identify a scripting language supported by the UA. IE and WebKit fetch the script, fire the load event for it but don't execute the file as a script. Firefox and Opera bail out early and don't fetch the file.

HTML5 specifies the IE/WebKit behavior for execution order and the Firefox/Opera behavior avoiding network traffic for unsupported script types.

Gecko's previous execution order behavior isn't really desirable. Specifically, enforcing insertion order execution among script-inserted inline scripts and script-inserted external scripts leads to site-breaking surprising effects when a previously inserted script-inserted external script is pending and causes a subsequent script-inserted inline script not to execute synchronously. To address this problem I recently implemented what HTML5 specifies in Gecko.

While doing that, another undesirable characteristic of Gecko's old behavior got fixed. Previously, script-inserted external scripts would block subsequent parser-inserted scripts which made performance tricks such as http://www.nczonline.net/blog/2009/07/28/the-best-way-to-load-external-javascript/ not work as advertised in Gecko when there were subsequent parser-inserted scripts. Such tricks work in IE and WebKit and now, after the HTML5-compliance change, in Gecko, too.

After making the HTML5-compliance change in Gecko, it was brought to my attention (http://blog.getify.com/2010/10/ff4-script-loaders-and-order-preservation/) that there's a library called LABjs (http://labjs.com/) that provides the ordered execution of script-inserted external scripts as a service to JS apps and hides the lack of interoperability across browsers. There's also an "order" plug-in for RequireJS that provides the same service (http://requirejs.org/). They do (and want to do) this cross-origin without a CORS opt-in. (By default, RequireJS works with scripts that don't have interdependencies an script file evaluation time and only have interdependencies once you call into the APIs provided by the scripts.)

LABjs and the RequireJS order plug-in accomplish this by having two code paths: A Gecko/Opera path and another (de facto IE/WebKit) path. If the JS expression
(window.opera && Object.prototype.toString.call(window.opera) == "[object Opera]") || ("MozAppearance" in window.document.documentElement.style)
evaluates to true, LABjs and the RequireJS order plug-in run the Gecko/Opera path. Otherwise they run the code path the assumes the browsers has IE/WebKit traits.

For the current versions of LABjs and the RequireJS order plug-in to work as advertised, a browser where the above JS expression evaluates to true has to make script-inserted external scripts (that don't have the async attribute) maintain order among themselves and a browser where the JS expression evaluates to false has to load script-inserted external scripts that have an unsupported type, fire the load event for such loads and execute script-inserted external scripts (that don't have the async attribute) in the insertion order when the external resources are already in the HTTP cache.

My untested understanding of the failure modes is that an HTML5 compliant browser where the JS expression evaluates to true would break a LABjs or RequireJS order plug-in-using site if the site actually happens to load scripts where the execution order of the scripts matters, but there'd be no breakage if the site wasn't actually relying on the ordering property (e.g. if the scripts being loaded don't call into each other until you call a function exposed by one of them and you don't call into any of the functions provided by the script files before all of them have loaded). Since with the order plug-in for RequireJS you have to explicitly opt into using the order plug-in, chances are that sites that use the order plug-in *will* break. My untested understanding is that in a HTML5-compliant browser where the above JS expression evaluates to false, breakage would be more severe regardless of whether the site really relies on the ordering property.

At this time, I don't have enough data about how LABjs is used, how widely it and the order plug-in for RequireJS are used and by how actively maintained sites to have a fully informed opinion on whether what I described above merits spec changes or willful violations of the spec in browsers in order to deal with legacy instances of LABjs and the order plug-in for RequireJS. However, my current not-fully-informed *guess* (which differs from my guess yesterday, FWIW) is that at minimum, the above-described willful violations in implementation are required (one willful violation for Gecko and Opera and another for WebKit and IE) until a better way of getting ordered execution is available in the spec and the installed base of LABjs and RequireJS has been updated to first try capability sniff the new feature and use it if available.

But leaving things to UA sniffing with two sets of browsers having two different willful violations of the spec isn't really a satisfactory way forward. In particular, it would mean that Gecko and Opera would be permanently at a slight performance disadvantage compared to IE and WebKit when the execution order of script-inserted external scripts doesn't matter (and the author doesn't bother to use the async attribute to use the trick that already works in Firefox 3.6 to opt out of ordering) and, on the other hand, IE and WebKit would permanently be at a slight disadvantage compared to Gecko and Opera when the order does matter (unless they implemented -moz-appearance or the opera object, of course!).

For future versions of LABjs, the order plug-in for RequireJS and similar libraries, I think we should provide a capability-sniffable opt-in for at least making script-inserted external scripts that have opted in to ordering execute in the insertion order. Perhaps this feature should extend to allow even script-inserted inline script to opt into participating in the same queue as opting-in script-inserted external script.

I think that both LABjs and the order plug-in for RequireJS meets our typical bar for establishing demand by seeing what limitations of the platform Web developers try to work around using JS or Flash.

Here's my proposal for the opt-in feature:

In the HTMLScriptElement interface, add |attribute boolean ordered;| which doesn't reflect any content attribute and defaults to false. Scripts could capability sniff this feature by evaluating ("ordered" in document.createElement("script")). Introduce a new list of scripts called "list of script-inserted scripts that execute in order". In the run algorithm, if a script has not been marked as parser-inserted and the script has the DOM property ordered set to true, put the script on the "list of script-inserted scripts that execute in order" and if the script's text is already available, post a task to process scripts that are ready to run. (Note: Script-inserted inline scripts with .ordered=true wouldn't execute at this point even if the list were currently empty.) When a task to process scripts that are ready to run fires or when the network task queue has finished fetching an external script, run execute the first item on the "list of script-inserted scripts that execute in order" and repeat until the list is empty or the first item is an external scripts whose text hasn't been fully fetched yet.

Thoughts?

-- 
Henri Sivonen
hsivonen@iki.fi
http://hsivonen.iki.fi/

Received on Friday, 8 October 2010 09:08:36 UTC