Proposal for Delayed Script Execution
Presently, HTML5 documents are limited in the number of ways a script can be loaded:
- By default, a <script> tag immediately begins to download an external JavaScript file and executes when the download is complete. Multiple <script> tags placed in order will always execute in order, blocking further execution and resource downloads until each script is executed. User agents may download the external files in parallel but always execute the scripts in order.
- When a <script> tag is marked with defer, the download of the external file begins immediately but execution is deferred until after the DOMContentLoaded event and before the load the event. Deferred scripts do not block but do preserve the order of execution.
- When a <script> tag is marked with async, the download of the external file begins immediately and execution occurs as soon as the file is completely downloaded. An async script does not block other resources from being downloaded. Multiple async scripts do not preserve the order in which they are specified, instead each executes as soon as it is completely downloaded. This occurs before the load event.
Each approach solves a very specific problem but no approach solves them all. For instance, there is no way to download a JavaScript file and delay it’s execution until a later point in time that can be defined by the developer (deferred scripts are the closest, but the execution point is predetermined and unchangeable).
Goal
This proposal is intended to describe a mechanism whereby JavaScript can be included in an HTML page, either via inline script or external file, and the parsing and execution is delayed until a time determined by the developer.Rationale
Currently, there are various script loaders such as LAB.js and ControlJS that seek to alter the way that scripts are loaded onto a page. Each script loader operates on their own theory of how best to load script files, whether to optimize for parallel downloads or delay execution until a later point in time. After some consideration, it seems that developer-controlled execution time of JavaScript is a minimal feature that would enable parallel downloads as well as other execution paradigms.Requirements
- The functionality must be exposed to feature detection techniques.
- Avoid double downloads of a single file.
- Don’t inhibit the ability to download files in parallel and execute in any arbitrary order.
Proposal Summary
Allow scripts to be marked as “preload”, indicating that the script should be downloaded (in the case of external scripts) but not executed immediately. Since the script isn’t being executed, it must not block other page resources from being downloaded (which would allow parallel downloading). Developers may instruct the script to execute at a later point in time.This proposal is based largely on the Internet Explorer implementation for dynamic script nodes. In that implementation, the external script file begins to download as soon as src is assigned but the script is not executed until the node is added to the document. This is a nice feature but suffers from several drawbacks:
- No way to feature detect that this behavior happens (as opposed to other browsers that only begin to download when the script node is added to the document).
- No way to determine when the script is downloaded but not ready for execution.
This proposal seeks to build upon this behavior to fix these shortcomings.
Proposal Details
HTML Usage
There is no HTML usage for the preload attribute. If an attribute named preload is included in an HTML <script> tag, it must be treated as an unknown attribute.JavaScript Usage
Scripts may be dynamically loaded from JavaScript by creating a <script> element and setting its preload property to true (default value is false). Example:var script = document.createElement(“script”); script.preload = true; script.src = “foo.js”;
When the src attribute is assigned, the JavaScript file should be downloaded per user agent default and stored in the appropriate user agent cache(s) but not executed. User agents may background parse or compile the script in preparation for execution but must not execute the code until instructed to do so.
In order to execute the code, the script node must be added to the document:
document.body.appendChild(script);
Upon adding into the document, the contents of the external file referenced by the <script> element must be executed synchronously in the global scope per usual user agent behavior.
Additional HTMLScriptElement Members
In order to accurately detect and react to changes in script state due to this proposal, several changes and additions are necessary to the HTMLScriptElement.interface HTMLScriptElement : HTMLElement {
attribute DOMString src;
attribute boolean async;
attribute boolean defer;
attribute DOMString type;
attribute DOMString charset;
attribute DOMString text;
attribute boolean preload;
};
The only change is the introduction of a preload property. As discussed earlier, setting this to true prior to the src being set puts the script into preload mode, where code may be downloaded but not executed.
The default value for preload is false. When preload is false, the user agent may download and execute the external script according to its normal behavior.
For feature detection, a developer may determine if this proposal has been implemented by detecting the preload property:
var preloadSupported = (typeof
document.createElement(“script”).preload == “boolean”);
Changes to HTMLScriptElement Events
HTML5 defines two events: load and error. The load event fires on preload scripts only after the script has been downloaded and executed; the error event functions exactly the same for preload scripts.
Additionally, In order to fulfill the requirements of this proposal, it’s necessary to introduce a new event for HTMLScriptElement. The preload event fires with a preload script has been completely loaded and is ready for execution. You can therefore determine when a script has been preloaded using the following:
var script = document.createElement(“script”); script.preload = true; script.src = “foo.js”; script.onpreload = function(event){ document.head.appendChild(script); };To ensure a script has been preloaded properly, you should also listen for the error event.
Additional Behaviors
Since preloaded scripts alter the behavior of dynamic scripts slightly, there are a few edge cases to consider.- Any attempt to set the text property on a dynamic script element already marked with preload is ignored. If the preload property is set to true after the text property is set to a value, then the preload property is ignored and remains false.
- If cloneNode() is called on a script marked as preload, then the cloned script node cannot be executed.
Possible Augmentation
One possible augmentation of this proposal is that preload automatically be set to true for script elements created using JavaScript, thus allowing developers to opt-out of this functionality by setting to false rather than opt-in. This likely would work since this is the default behavior in Internet Explorer already.Examples
Mimic deferred scripts
The following is an example of using this proposal’s functionality to simulate deferred scripts in JavaScript. Note that real deferred scripts fire before DOMContentLoaded, while this will fire during.
(function(){
var domLoaded = false,
script = document.createElement(“script”);
script.preload = true;
script.onpreload = function(){
if(domLoaded){ //just in case it’s too late
document.body.appendChild(script);
} else {
script._ready = true;
}
};
script.src = “foo.js”;
document.addEventListener(“DOMContentLoaded”, function(){
if (script._ready){
document.body.appendChild(script);
}
domLoaded = true;
}, false);
})();
Script Loader with Parallelization
The following is an example of a script loader that allows scripts to download in parallel but execute in the order in which they are specified. This is done by creating preload scripts for each external JavaScript file and waiting until they are all loaded. Once all are loaded, a loop is used to execute each in order.
var ScriptLoader = (function(){ var batches = []; function handlePreload(batchId, scriptId){ var batch = batches[batchId], script = batch.scripts[scriptId]; batch.loaded++; //all scripts are now loaded if (batch.loaded == batch.scripts.length){ for (var i=0, len=batch.scripts.length; i<len; i++){ document.body.appendChild(batch.scripts[i].scriptNode); } batch.success(); } } function handleError(batchId, scriptId){ var batch = batches[batchId]; batch.error() } return { loadScripts: function(urls, successCallback, errorCallback){ var batch = { scripts: [], success: successCallback, error: errorCallback, id: batches.length, loaded: 0 }, i=0, len = urls.length, script; batches.push(batch); while(i < len){ script = document.createElement("script"); script.type = "text/javascript"; script.preload = true; script.onpreload = (function(scriptId){ return function(){ handlePreload(batch.id, scriptId); }; })(i); script.onerror = (function(scriptId){ return function(){ handleError(batch.id, scriptId); }; })(i); batch.scripts.push({scriptNode: script, id: i}); script.src = urls[i]; document.head.appendChild(script); i++; } } }; })(); //usage ScriptLoader.loadScripts([“foo.js”, “bar.js”], function(){ foo.init(); }, function(){ alert(“On no!”); } );