A user account is required in order to edit this wiki, but we've had to disable public user registrations due to spam.

To request an account, ask an autoconfirmed user on Chat (such as one of these permanent autoconfirmed members).

Dynamic Script Execution Order

From WHATWG Wiki
Revision as of 15:23, 30 October 2010 by Getify (talk | contribs) (→‎Benefits)
Jump to navigation Jump to search

This page is intended to formalize the discussion around an important but currently underserved use-case (in both spec and various browser implementations): the need for a dynamic script loading facility that can download resources in parallel but ensure that they execute serially in insertion order, for dependency reasons.

A long email thread on the W3C public-html list, which began here and for which a recent message is here, has been discussing this problem, but the email thread is becoming unruly and hard to track, so this wiki page will now be the official location to discuss the topic.

Briefly, this issue has arisen because recent "nightlies" changes in both Mozilla Gecko and Webkit have broken the ability for script-loaders like LABjs to be able to download scripts in parallel but ensure their execution order. As a result of the discussion, it's become apparent that both the spec for, and various current browser implementations around, dynamic script loading is incomplete in addressing this use case, and that some change needs to occur.

There is one main proposal that has surfaced from the discussions, with several other alternatives having been discussed. This page will try to distill the long email thread down and clearly present the main proposal, objections, and alternative suggestions.

Use Case Description

Script tags/elements can either:

a) appear in the HTML markup ("parser-inserted"), OR

b) be dynamically appended to the DOM using document.createElement() ("script-inserted")

Parser-inserted script tags, up until recent browser versions, had the undesirable performance behavior of loading and executing serially, blocking everything else while doing so. Recently, many browsers have improved the situation by loading the scripts in parallel, but still executing them in order.

On-demand (or dynamic) script loading has emerged in recent years for a variety of different reasons, most notably the performance improvements to address such concerns. It is desired by many different scenarios to be able to download scripts to a page completely independently of the loading of the rest of a page's resources, or even well after a page has finished loading ("on-demand").

Recent additions to HTML spec such as "defer" and "async" were intended to address the use-case for parser-inserted script elements, but their behaviors have been unhelpful for the on-demand loading use-case (script-inserted script elements).

Thus, script loaders (like LABjs) were developed to give page authors an easy way to specify one or more scripts to load (regardless of when in the life-time of the page that loading is requested), and for as many of them as possible to load in parallel, and for those scripts to be loadable from any local or remote domain location, and for the script loader to be able to control the execution order (if the usage of the script loader's API expresses the need to) to preserve dependencies between the scripts.

Sometimes, dynamic script loading is used to load totally independent scripts, and thus "as fast as possible" execution is desired. Other times (and possibly more frequently), multiple scripts are loaded with some dependencies among them, requiring them to execute in a certain order.

What is needed is some facility by which a script loader can express that dynamic script loading either does or does not need execution order preserved among the queue of requested script loadings.

Current Limitations

Unfortunately, browser behavior around script-inserted script elements and their loading and execution behavior is splintered. There are, at present, two main camps of behavior:

a) in IE and Webkit (including Chrome), the default behavior for script-inserted script elements is for them all to execute in "as fast as possible" mode, meaning there's no guarantee about ordering. This effectively makes on-demand (dynamic) script loading impossible to work in parallel-mode if the resources in question have dependencies -- the only straight-forward way to handle things is to load each file and execute, serially, losing the parallel loading performance benefits.

b) in Gecko and Opera, the default behavior for script-inserted script elements is for them all to load in parallel, but execute serially in insertion order. Technically, this makes it impossible to dynamically load independent script elements and have them execute in "as fast as possible" mode.

As described above, both behaviors are desirable under different circumstances, but each of the two camps provides only one behavior, and no way to straightforwardly achieve the other. This obviously creates a big nightmare interoperability-wise when trying to provide a general script loader cross-browser.

Since it is observed that the behavior in (a) is more detrimental (race-condition wise) for the case where dependencies exist, script loaders like LABjs had to find some way around this problem while still attempting the best possible parallel loading performance. However, the trick used is hacky and not completely reliable -- yet it is the best way to solve the use-case in IE and Webkit.

For Gecko/Opera, the concession was just (silently) made that the lesser-common use case of "as fast as possible" wasn't possible, but degraded fine to "insertion order execution", while keeping the parallel loading benefits.

Recently, Gecko landed a patch to the trunk that stopped Gecko's existing behavior of preserving execution order, making script-inserted script elements now execute in "as fast as possible" mode similiar to IE/Webkit. Unfortunately, the "workaround" used in IE/Webkit (described in the next section) for dealing with parallel loading and execution-order dependencies does not work in Gecko, which means presently the use-case in question is now broken in Gecko trunk/FF4 nightlies.

Moreover, Webkit recently landed a patch to the trunk that stopped Webkit's non-standard but long-held behavior (also used in the "workaround" described in the next section) of loading into cache script resources with an unrecognized "type" value, but silently not executing them. This behvaior (while hacky) is central to being able to address the use-case in question in Webkit, so at present, Webkit nightlies are now also entirely broken on the use-case (though in a different way than Gecko).

Both the Gecko change and the Webkit change are well-intentioned, as they are bringing the respective browser more in line with the HTML spec. However, what's really been demonstrated is that the HTML spec is not properly handling this use-case, and so the goal is not to address the browser issues raised with more awkward hacks, but to address the shortcomings of the spec first, and then encourage all browsers to adhere to such.

Current Usage and Workarounds

To work around the limitation in IE/Webkit(prior to the above noted patch) of not being able to rely on script order execution for parallel loaded scripts, a "preloading" trick was developed. This trick relied on non-standard (but long-held) behavior in these browsers that a script-inserted script element with an unrecognized "type" value (such as "script/cache") would be fetched/loaded, but would not execute. This had the effect of loading the resource into cache, and then firing the "load" handlers to let the page know when the resource was completely in cache.

Assuming that the resource was served with proper caching headers, and was in fact in the cache, it could then be executed (nearly) immediately when it was the proper execution order time by re-requesting the same resource via another script-inserted script element with the proper "text/javascript" type value, pulling the resource from the cache and executing it, without another server round-trip.

Of course, the assumption of proper cache headers is a huge one, and not at all reliable. Some recent estimates by performance optimization specialists have suggested as much as 70% of scripts across the internet are not served with proper caching headers, which means that such scripts would be completely ineffective if loaded using this (or a similar) technique. The script resource would end up being loaded completely a second-time, and the "near immediate" execution would obviously be false, and thus race conditions would ensue.

It's important to note at this point that the new <link rel=prefetch> facility has been suggested as a better workaround, but it suffers the same ill-fated assumption of script cacheability. Still others have suggested "new Image().src=..." or the <object> preloading trick suggested by Stoyan Stefanov. Again, these tricks unwisely assume cacheability, for the "preloading" trick to solve the use-case in question.

At Risk

Currently, there are several large/popular web sites which are either currently (or who intend to soon) use LABjs in such a way as to run afoul of the new Gecko and Webkit behavioral changes with LABjs failing to operate properly. It's important to note that the problems of race conditions can be subtle and hard to detect, and so merely loading up such sites and failing to observe overt failure is not sufficient.

Sites which are known to have LABjs loading techniques in place with currently broken (or susceptible to such breakage in the near future) behavior are:

Rather than getting hung up in the syntax of usage in LABjs that is or is not going to break, it's best to just think of the problem this way:

Does a site need to load more than one script, at least one of which comes from a remote domain location (like a CDN), and for which among the scripts there's at least one execution-order dependency among them? If so, then that site is susceptible to the current/future breakage if the HTML spec (and browsers) do not address this use case.

A common example (in use on many sites) of such might be loading:

  • jQuery from the CDN
  • jQuery-UI from the CDN
  • plugins and usage code in one or more local files
  • Google Analytics from the Google domain

Note that the emergence of popular script frameworks and their hosting on public CDN's is leading to more and more sites loading scripts from both local and remote locations, and also to loading more files that have dependencies (rather than the practice of concat'ing all files into one file to avoid dependency issues).

Any site which fits a profile like the above, and which might currently (many do), or in the future want to, use a script loader to improve their loading performance, will fail to achieve what they want cross-browser and in a performant way, if the HTML spec (and browsers) do not address the use case.

Benefits

The benefits of addressing both behaviors directly (without "preloading" tricks and bad assumption reliance) have been implied in the above discussion, but in short are:

a) clear and simplified code for script loaders, which leads to easier use by authors of more pages, which in turn leads to better web performance (as demonstrated clearly by intelligent script loading techniques as compared to just simple <script> tags in HTML markup)

b) full access to either/both of the execution-order behaviors (as the author sees fit), regardless of browser

c) avoiding reliance on bad assumptions (like cacheability) as a sufficient way to address the use-case

Requests for this Feature

Proposed Solutions

My Solution

The current proposal most well supported from the email discussion thread, and the one which I feel makes most sense, is described here.

The HTML spec already defines the "async" attribute for parser-inserted script tags, which when set to "true", changes their execution order behavior to be like script-inserted script-elements (in IE/Webkit), which is that they load in parallel and execute "as fast as possible" (ostensibly because the author is expressing no dependencies between multiple such "async"-marked scripts). Parser-inserted script elements without "async" (or with it set to false) behave as before and expected, which is that they load in parallel but execute in order.

However, the HTML spec does not define the "async" property (or any such behavior) for script-inserted script nodes (such as those created by a script loader). Instead, the spec implies that "async=true" like behavior is always true for such script-inserted script elements.

What is proposed is:

a) Script-inserted script elements should have (and respect the value of) an "async" property which is basically identical to the "async" attribute for parser-inserted script elements. That is, script elements with the "async" property set to "true" will behave accordingly, as will script elements with the "async" property set to false.

Essentially, the proposal is to mirror the "async" attribute behavior of parser-inserted script elements as an "async" property on script-inserted script elements. This has the benefit of using an existing facility and extending it (from current spec) in a way that is sensisble and symmetric with its current definition.

b) Furthermore, to aid in "feature-detection" of such new behavior, the proposal is to have the default value for the "async" property of script-inserted script elements be "true" (and of course the associated behavior thereof).

There are two major benefits to (b). One is that it provides a way to feature test such new behavior by not just looking for the existence of the "async" property on script elements, but specifically that the default value is "true" (which is opposite of what it would currently/normally be). Secondly, defaulting to "async=true" behavior for script-inserted script elements would preserve the default behavior of IE and Webkit, meaning there'd be less of a chance of breaking existing web content in either of those two browsers.

It's also important to note that there is no implied or requested effect or dependency between script-inserted script elements and parser-inserted script elements -- the two types of scripts would load and execute in entirely separate behavioral sandboxes.

Proposal Amendment: Events

In addition to standardizing how scripts load and execute, it's also important to standardize the load and error events, and under what circumstances they fire, etc. Without reliable load/error events, the main proposal is not reliable and is thus unhelpful for the use-case.

A script-inserted script element must fire the "error" event only once, when:

a) immediately after the resource finishes loading, but before parsing and execution, AND

b) the script element loads any non-empty content (that is, it was a successful HTTP request), AND

c) the script element loads has either loaded from the remote location, or is loaded from the brower cache

Specifically, the script "load" event must come immediately between the script element finishing loading and the script element being parsed/executed. If the script resource successfully loads, nothing should interrupt the sequence of fininshing loading, the "load" event firing, and the script proceeding to parsing/execution.

A script-inserted script element must fire the "error" event only once, when:

a) immediately after a loading failure for the resource is detected (that is, an HTTP error received, such as 404, 500, 503, etc).

Either the "load" or "error" event, but not both, will fire, and only once, for every script-inserted script element.

NOTE: "load" and "error" events on script-inserted script elements must fire synchronously to ensure event reliability.

A case may be made for adding the "load" and "error" events as described here to parser-inserted scripts as well.

Processing Model

Script inserted script elements will have an "async" property that defaults to "true". If the author does not change the value, all such requested script loadings will be in their own "queue" and will default to "as fast as possible" execution behavior. For any script elements that the author sets "async=false", those scripts will load in a separate "queue", and will execute in insertion order only. Again, these two "queues" will operate strictly independent of each other.

Limitations

The two behaviors being identified solve either the case where no dependencies exist, or where a linear dependency chain exists (and which the script elements can be requested to execute in that order). It is possible that some authors have a more complex non-linear dependency chain that they would like to express. This would obviously require a much more complex API and spec change, and since that use-case has not (yet) surfaced as a particularly main-stream request, I believe it would be overengineering to try to address it with this proposed change set.

In addition, it's been duly noted that it's undesirable (and potentially confusing/problematic) to intentionally build in the inconsistency of having the "async" attribute (for parser-inserted scripts) and the "async" property (for script-inserted scripts) have different default values ("false" for the attribute, "true" for the property).

It's also been a point of discussion whether or not such a spec change has enough "carrot" to entice the browser vendors (namely IE and Webkit) to implement the behavior. Moreover, there's been some concern that if the default value for the "async" property were made to be "false" (like the attribute) to be more consistent and conservative, then it would possibly give the perception to IE and Webkit of "losing" some performance to cut out their default "as fast as possible" behavior.

Alternate Proposals

One early proposal on the email list was to introduce an entirely new property like "ordered" which an author could add to a script-inserted script element to instruct the browser to put it into the queue of execution-order-preserving script loadings. While such a property would address the use case, it introduces another property and thus more complicates the issue. It also fails to address the current spec inconsistency (which is confusing to new comers) that "async" is not a present/respected property in mirror of the attribute of the same name.

Another suggestion was a "waitFor" property that would be added to script elements and would specify a list of one or more DOM id's of other script elements that the current script should "wait for" in terms of execution. Again, this would solve the use case, but in a more complicated way, and there are concerns that it would be too confusing for the normal use-case.

Several suggestions have come in the form of creating explicit "preloading" (similar to <link rel=prefetch>), but as described above, "preloading" to solve this use case is a non-ideal hack and highly susceptible to breakage if the script fails to be sent with proper caching headers.

It's also been suggested that since this type of behavior is somewhat complicated, it may be better to intentionally obfuscate or complicate any such facility in the HTML, so as to make the barrier-to-entry rather high and force users to know what they are doing before doing it.

It's been suggested that "defer" already preserves execution order. However, "defer" is only defined for parser-inserted scripts, and thus is not applicable to the use-case in question from an on-demand point of view. Also, "defer" scripts explicitly way for DOMContentLoaded, even if they're ready to execute sooner. So this is less than desired.

Yet another proposal is a "document.executeScripts()" API, where an author can specify multiple sets of scripts that can load in parallel and it will enforce their execution order. A variation on that same idea was to use the "importScripts" from the Web Workers spec, however "importScripts" is synchronous (undesirable performance wise, obviously). The main downside (besides extra API complication) to "document.executeScripts()" is that there seem to be quite a few script execution properties/behaviors (including "document.currentScript" and charset override) which would have to be duplicated into this API facility.

"Script Group" element

One recent alternate proposal bears some special consideration, as it seems like it might be a decent option (although certainly more of a radical change for the spec and for browsers to implement). But it has the appearance of being pretty straightforward and semantic for authors to use, perhaps even more so than using "async".

The proposal is to create a new HTML element, called perhaps <scriptGroup>, <collection>, etc. Specifically, this element must be able to be inserted wherever a <script> element can currently be inserted. The "script group" element is intended to signify that all script elements added to it must perserve insertion execution order. This element wouldn't have much (but still some, explained in a moment) meaning in a parser-inserted context, since parser-inserted scripts already preserve order among themselves.

An advantage of the "script group" element would be to give a direct and easy way to attach event listeners ("load" and "error") to the entire group, rather than having to internally track events for each element if all you care about is the final "load" event, for instance. In the case of event handling, the "script group" element would have perhaps some benefit even in parser-inserted (markup) context.

The element would need to have an "id" property, and possibly attributes for "load" and "error" events.

A variation on how to look at this proposal is that a "script group" element could have an attribute/property on it (perhaps called "ordered") which would allow the group to either be order preserved or not. This would make the "script group" element much more useful in the parser-inserted context, as it would sort of be a shortcut to setting "async=true" on all the child script elements.

The "script group" element might look like this:

 <scriptGroup id="group1">
   <script src="foo.js"></script>
   <script src="bar.js"></script>
   <script>
     somethingInline();
   </script>
 </scriptGroup>

Or, with the "ordered" attribute to explicitly control ordering behavior for the group:

 <scriptGroup id="group1" ordered="true">
   <script src="foo.js"></script>
   <script src="bar.js"></script>
   <script>
     somethingInline();
   </script>
 </scriptGroup>
 <scriptGroup id="group2" ordered="false" onload="alldone();">
   <script src="baz.js"></script>
   <script src="far.js"></script>
   <script src="zab.js"></script>
 </scriptGroup>

While the "script group" element is definitely more complicated to define and implement, it does have some semantic advantages for authors, and it also would significantly reduce the internal complexity of script loaders like LABjs. It would give authors (either directly or through script loaders) the flexibility to group scripts together into one of the two aforementioned behaviors (execution "as fast as possible" or "in insertion order"), and to easily access both behaviors in the same page.