Script Execution Control
This page is intended to formalize the discussion around the ability to control (aka, defer until desired) the execution of a script, to occur sometime after the script finishes loading.
This feature, with two main proposals for how to accomplish it, has been discussed on in a long email thread on the WHATWG list: "Proposal for separating script downloads and execution"
This page is being created to document that first proposal, as well as a second proposal (which actually came as a result of feedback on another WHATWG wiki page/proposal: Dynamic_Script_Execution_Order) for the same functionality. These are the two main solutions being discussed at the moment. This page will also attempt to distill some of the feedback and other (unfortunately unsuitable) proposals discussed thus far.
Please feel free to join the discussion of this topic in the Talk:Script_Execution_Control area.
- 1 Use Case Description
- 2 Proposed Solutions
- 3 Insufficient Solution Suggestions
Use Case Description
The use-case is that one or more scripts may be necessary to download, but not to automatically execute. The reasons for wanting to defer the execution of a script element until on-demand later are varied. They include performance issues with limited CPU's on mobile devices, the ability to choose based on user-action which of two scripts should be applied, the ability to delay the execution of a script until the user-action activates some part of a page, etc.
In addition, this functionality as described will be useful for parallel script loaders, giving them the ability to download multiple scripts in parallel, but control their execution order by deferring the execution of all of them, and then once they are all loaded, executing them on-demand in desired order. The base-version of this application of the use-case was addressed by the "async=false" behavior, recently adopted by the Specification, but would be bolstered and improved by the functionality in these current proposals.
There's no way to defer the execution of a script element, except by use of the `defer` attribute in markup script elements (meaning it's inaccessible to dynamic script loaders). However, the `defer` attribute in markup still only allows deferring until after DOMContentLoaded (aka, "DOM-ready"). Several applications of the use-case call for an abitrary, author-controlled point to decide when scripts should execute, not locked to DOM-ready.
With regards to the "async=false" behavior being improved with this proposal, script loaders cannot currently (with "async=false") specify multiple "groups" of scripts which have order-dependency within the group, but the groups themselves are completely independent. There's no way to currently download all scripts in groups 1 and 2, but allow either group 1 or group 2 to execute when the first one finishes. Currently, the only way to do this based on specification is to use "async=false", and then all the scripts will execute in the order added to the DOM -- the "independent grouping" of execution is not possible.
Current Usage and Workarounds
Currently, this functionality is available in IE (since version 4). To achieve this functionality in other browsers, there's two main types of hacks to accomplish it:
1. The content may be "wrapped", for instance in /* ... */ comments, such that its automatic execution will be moot. Then, the page can, at the desired time, grab the script content, remove the comment markers, and re-execute the code. This technique only works for inline script elements, not for externally loaded script elements (which also means that any caching benefit is lost).
2. The page can employ one of a variety of "cache preload" tricks (different for each browser), whereby the script is loaded into the cache but not executed, and then when execution is desired, the script is re-requested with a proper script element, and thus executed. The main drawback to this approach is the assumption of cacheability. If a script was not properly cached, this technique will result in a costly double-load. Also, as stated, these hacks are brittle and require different variations for each browser. They are quite susceptible to future restrictions by the Specification, or by changes to browsers' optimization techniques.
The primary benefit underlying all the use-cases of this proposal is performance. It gives the page more control over execution of downloaded scripts, allowing the page author to make tradeoffs on when scripts are executed in an attempt to optimize for the user-conditions and experience.
The secondary benefit is to reduce (greatly) the complexity (and hackiness) of script loaders (like LABjs, ControlJS, etc) which go to great lengths to employ tricks and hacks to accomplish this described functionality in existing browsers.
External Discussions of this Feature
Proposal 1 (Nicholas Zakas)
This proposal is described in detail in Nicholas Zakas' Delayed Script Execution v2.1, but will be summarized here for clarity of this page.
This proposal recognizes the need for an event mechanism to detect when a script is preloaded, as well as the need for a feature-detect to detect if the functionality is implemented in a browser.
The proposal is:
1. Add a `preload` property to dynamic script-created script elements (*not* markup script elements), which defaults to false, that can be set to `true` *before* the setting of the `src` property. Turning on "preload" will cause the script to be loaded immediately, even if the script has not yet been added to the DOM.
2. Add a `onpreload` event, which fires when the script element finishes preloading. The script element can then, at that event time, or later, be executed by adding the script element to the DOM through normal procedures.
3. The feature-detect will be:
(typeof document.createElement(“script”).preload == “boolean”)
The goal of this proposal is to be as semantic as possible to what is actually occurring (thus the use of the very descriptive and appropriately named `preload` and `onpreload`).
1. This proposal's main limitation is that it requires a new property and a new event (which is not precedented anywhere else in the HTML specification yet). That means that the proposal will require more effort on the part of the Specification as well as on the part of browsers to implement.
2. In fact, for full adherence to this proposal, it will actually require a change to IE's existing behavior, to make it not "preload" if the `preload` attribute is not set. This is quite possible to create backwards-compat concerns from the IE team.
Getify 15:24, 14 February 2011 (UTC)
2. This proposal requires changes from IE to achieve full-compat. As IE is known to have a much slower release timeline, and IE9 is now feature-complete and nearing a full release soon, the likelihood is that this current IE cycle has been missed for any such changes. That means full-compat would probably be at best 1-2 years from now.
Getify 15:27, 14 February 2011 (UTC)
3. DOM event propagation (discussion moved to: Talk:Script_Execution_Control#Discussion_for_18.104.22.168_Limitations)
4. Run-away memory usage (see 22.214.171.124)
5. Doesn't support inline script "preloading" (see 126.96.36.199)
6. This proposal doesn't specify fetch error handling. onerror is currently not fired on scripts which are not in a document.
Proposal 2 (Kyle Simpson)
This proposal is, in spirit, very similar to Proposal 1, but is a little bit simpler (in that it doesn't add a new property or unprecedented event mechanism). Whereas Proposal 1 seeks to add a `preload` attribute to the script element to signal that it should have the preloading (load-but-not-execute) behavior, this proposal builds on automatic behavior for preloading already suggested by the current HTML Specification:
Specifically, in step 14 (which is about fetching a `src` URL):
For performance reasons, user agents may start fetching the script as soon as the attribute is set, instead, in the hope that the element will be inserted into the document. Either way, once the element is inserted into the document, the load must have started. If the UA performs such prefetching, but the element is never inserted in the document, or the src attribute is dynamically changed, then the user agent will not execute the script, and the fetching process will have been effectively wasted.
Put simply, this wording says that a script may begin downloading as soon as the `src` property is set on a dynamic script element, even if that element is not added to the DOM. Further, it suggests that adding the element to the DOM is necessary to execute the script. From this, we can infer that a script may be "preloaded" by creating and keeping references to script elements, but not adding them to the DOM. And then when the script is desired to execute, the script element can be added to the DOM.
In addition to this wording already being in the specification, the functionality as described has been implemented in IE since version 4.
As with Proposal 1, this proposal also requires an event mechanism which can allow the web author to be notified of when a script element is finished preloading. In addition, both proposals require a "feature-detect" so that web authors can opt-in to the described behavior after detecting that it is present in a browser.
The proposal is:
1. Change the above referenced wording (as necessary) to indicate that the preloading behavior is required rather than being a performance suggestion.
2. Taking IE's implementation as the pattern, specify the `readyState` property on the script element (defaults to "uninitializd"), and fire `onreadystatechange` events when the script is "loaded" (finished preloading) and "complete" (finished executing, ~ similar to `onload`). The `readyState` functionality is already precedented in the specification for the XHR object, so that same pattern and wording could/should be applied here.
3. The feature-detect will be:
(document.createElement("script").readyState == "uninitialized")
The reason for checking not only the `readyState` variable but also its value is that Opera currently implements a non-functional `readyState` property on the script element (but does NOT support the proposed functionality), but Opera's default value is "complete". The feature-test would give a false-positive in Opera if the value isn't included in the test. Opera has stated in the WHATWG discussion thread that they would not not change the default value of `readyState` to "uninitialized" unless they were also implementing the behavior described in this proposal. No other known browsers have `readyState` on script elements, and the assumption is that any new browser implementations adopting it would adhere to the specification (if this proposal is adopted). So, the proposed feature-detect should be reliable.
This proposal allows scripts to be added to the DOM before they are finished loading, as they currently can, with no change in current behavior. It also does not specify any different model for script execution than is currently present in browsers. In other words, under the circumstances that a browser would execute a script synchronously, the same would be true, and under the circumstances a script would be queued for execution asynchronously as soon as possible, the same would also be true.
The goal of this proposal is to make the minimum change to both specification and browser behavior necessary to fulfill all the goals of the stated use-case. It does not introduce any new, unprecedented elements or concepts, and instead builds on existing specification wording and actual browser implementation.
1. The feature-detect is riskier/weaker/more brittle, and not as graceful as in Proposal 1
- Yes, it's less graceful of a feature-detect. But as explained, it *is* functional. Given appropriate evangelism and involvement with browser vendors, if this proposal were implemented as described, the feature-detect should not be risky.
2. The proposal does not address the use-case of deferring inline script elements' execution
- Yes, neither of the proposals are seeking to fill this use-case, but this proposal is not really capable of it, whereas the other proposal *could* be extended to include markup script element support for that use-case. There's not yet been sufficient evidence of that use-case being necessary.
3. All the preloading proposals suffer a potential for run-away memory consumption if a script misbehaves and causes a bunch of "preloaded" scripts that it abandons and never attaches to the page to execute. The browser would have to hold on to that memory for the lifetime of the page, assuming that one might be used at some later time on the page.
This particular proposal makes this a little bit more likely since it's easier (automatic, rather than opt-in) to make the mistake.
Two suggestions for how to address:
a) implement some content-uniqueness in-memory cache which only keeps one copy of each unique set of resource content in memory, to prevent needless duplication. the algorithm for how to do this correctly is probably quite complicated to actually do.
b) implement a cap on the number of scripts that will be preloaded, for instance at 200 or 500. If the cap is high enough, almost all normal sites which will use preloading will never need that many scripts waiting in a preloading queue. And the run-away sites will be prevented from consuming more than that cap. Any script preload requests above the cap will simply sit and wait for a preloading slot to become available. The resolution is to simply start executing some scripts by adding them to the page, thereby freeing up more preloading slots.
4. DOM event propagation (discussion moved to: Talk:Script_Execution_Control#Discussion_for_188.8.131.52_Limitations)
5. This proposal doesn't specify fetch error handling. onerror is currently not fired on scripts which are not in a document.
Proposal 3 (Glenn Maynard)
This proposal keeps changes to script preparation and execution to a bare minimum. Changes are described in terms of actual spec changes, to describe its behavior clearly and to show how it fits in with the existing spec. Briefly, a "noexecute" attribute is added to block execution of the script, which is cleared to reenable execution. More precisely:
- Add the following attribute definition:
The noexecute attribute is a boolean attribute that indicates that the script should not be executed.
- Add the following flag definition:
The fifth is a flag indicating that a script is "pending-execution". Initially, script elements must have this flag unset. This flag is used to trigger execution if execution was deferred due to a noexecute attribute.
- Add the following to "execute a script block", before "1. Initialize the script block's source as follows":
0. If the element has a "noexecute" attribute, set the "pending-execution" flag and abort these steps.
- For each part of step 15 "Then, the first ..." of "prepare a script", the noexecute attribute being set implies the async attribute is also set, eg. "... and the element has neither an async nor a noexecute attribute". That is, noexecute implies async.
- If the "noexecute" attribute is changed, and the "pending-execution" flag is set, clear the "pending-execution" flag and queue a task to execute the script block.
- When the fetch algorithm completes without error, fire a simple event named "fetch" at the element.
- Invariant: if the "pending-execution" flag is set, the "already started" flag is also set.
- The attribute name "noexecute" reflects its behavior more clearly than "preload". API naming should reflect what the API does, not name a use case.
- For feature detection: typeof(document.createElement("script").noexecute) == "boolean".
- Scripts are still only executed once, regulated by the "already started" flag.
- The "pending-execution" flag is not copied to clones, so a script that has run "prepare a script" will run again in a clone.
- The onfetch event is orthogonal to the rest, and should be fired whether or not noexecute is set. It's symmetric with onerror. (onreadystatechange is not recommended; it clashes with existing IE behavior. IE fires onreadystatechange with "loaded" for both success and error, making it hard to tell whether the load actually succeeded.) The "fetch" event isn't a new concept, it's simply the "load" of Progress Events-like contexts; a different name is used because script events already have a "load" event with a different meaning.
- <script noexecute> is comparable to <style disabled>, which can also be used for advance resource loading.
- The fetch event should be fired at most once, if the fetch algorithm finishes without error. That's not the top of "execute a script block" since that can be executed more than once. I'm not sure what the correct spec mechanism is to describe this.
- This could be extended slightly to support inline scripts, with additional modifications to "prepare a script" step 15.
- Feedback is that this approach is more likely to see implementation in Gecko, and it avoids potential issues related to fetching resources when not added to a document without needing additional resource limiting.
- DOM event consistency is maintained by not requiring elements be kept out of the document while loading. Fetch and error events can be captured, so event delegation continues to work consistently.
- Fetch error handling (DNS errors, 404 errors) is handled with no changes to current behavior.
- In addition to programmatic preloading, scripts can also be preloaded in markup and later activated by scripts.
1. From the markup perspective, this approach does not have graceful degradation, since in browsers without `noexecute` support, such scripts would execute right away, even though told not to. Markup doesn't provide the ability to feature-detect.
2. As stated in support sections for the other proposals, this proposal requires a fundamental change to how IE operates. As such, the feasibility of getting such a change into a released version of IE is, at best 18-24 months down the road, and that's if IE even agreed to change their existing preloading behavior.
Insufficient Solution Suggestions
One suggestion in the thread, which has come up numerous times, is that the already spec'd <link rel=prefetch> mechanism could address the use-case.
There are a few problems with this idea.
1. <link rel=prefetch> is currently only defined for <link> tags in markup. The main use-case is specifically about script loaders needing functionality from dynamica script-created script elements.
2. `prefetch` is defined specifically as a "hint" to the browser that it might benefit from loading a resource ahead of time. The description makes it clear the intended use is for preloading resources which may be used on another page. That's a poor fit for this use-case, because the script loader needs a specific way to say to the browser "I need this resource to load now, because I'm definitely going to use it later".
3. Once an element would be loaded via <link rel=prefetch>, the question arises as to how that resource can later be executed. There were two main options:
- Add an `execute()` method to the <link> element, that can be used to execute a script that was preloaded. This is problematic because of all the complicated functionality around script elements and their content's execution, and would require basically copying all those rules over to the <link> element, or at the very least morphing the <link> element into a <script> element at time of execution. This is most likely more trouble than it's worth.
- Simply re-request the resource at desired time of execution with a proper <script> element container. The problem here is that then the solution becomes just as fatally assumptive as the "cache preloading" tricks referred to earlier in this page. If the resource didn't/couldn't properly cache, the second request would result in a costly double-load. There's simply no way to guarantee that a resource can cache so that a second request can use it directly.
4. The link element does not currently have a well-defined event system that could inform a web author of when the "prefetch" had finished. As is clear from both the main proposals, an event mechanism is crucial for fulfilling the more advanced applications of the main use-case, and the whole system is not worth proposing without such an event notification.
all script content must be adjusted not to auto-execute
It has been suggested numerous times in the discussion thread that the simplest solution is for a script author to re-arrange the way a script is written, such that is has no directly executing functionality in its main code section. That way, the script is "safe" to execute (since it does nothing), and its functionality can easily be called upon at later times when actual "execution" is desired.
This argument misses the true nature of the use-case and proposal in two ways:
1. In the mobile-device world, especially, it has been observed that the mere "parsing" (that is, interpreting of a block of code for its function declarations) of a large chunk of code can be so CPU intensive as to overwhelm the device and cause slow-downs in the UI/UX, or even freezes for brief periods of time. The Gmail Mobile team for instance famously used the /* ... */ comment hack to hide code from the engine, which drastically reduced their UI latency issues on some mobile devices.
Many other experiments of this nature have been conducted, and the conclusion is that deferring the interpretation-execution of the script is critical to the use-case. So re-writing the script would do no good, unless it were to involve such hacks as the comment hack, which have obvious draw-backs to both the developer and the browser's caching/optimization efforts.