In Firefox 4, script execution changed to be more HTML5-compliant than before. This means that in some cases sites that sniff for Firefox or Gecko may break.
You should update to LABjs 1.0.4 (or later).
You should update to RequireJS 0.15.0 (or later).
You should switch to the single-file version of OpenLayers. (The upcoming 2.11 release is expected to fix the multi-file version.)
You don’t need to read further.
(However, if you triage bugs on bugzilla.mozilla.org, you might still want to read on.)
Note: This post is about when and how the script loader decides to evaluate scripts. This isn’t about what happens in the JavaScript engine once the script evaluation starts. Nothing discussed here has anything to do with JavaScript engine changes in Firefox 4.
To be able to discuss this topic precisely, let’s define some terminology.
An HTML script
element that has the src
attribute or an SVG script
element
that has the href attribute in the XLink namespace.
An HTML or SVG script
element that is
not an external script.
An HTML or SVG script
element that has
been instantiated by HTML parser parsing a script
tag from source code coming from the network stream or
document.write()
, by the XML parser parsing a script
tag from source code coming from the network stream or by the XSLT
processor when performing a transformation triggered by
<?xml-stylesheet?>
or by
XSLTProcessor.transformToDocument
()
.
A parser-inserted script can lose its parser-insertedness and become a script-inserted script if the script doesn’t start when the parser tries to run the script. This happens if the script is not in a document at that time or if the script is an inline script that doesn’t have non-whitespace text content.
An HTML or SVG script
element that is
not parser-inserted as defined above (including scripts created
using Range.createContextualFragment()
and XSLTProcessor.transformToFragment()
).
An external script whose .async
DOM
property getter returns true
.
A parser-inserted external script that is not an async script and
whose .defer
DOM property getter
returns true
.
The process of attempting to start a script. This may involve
fetching the script text over the network before executing the
script. (Blame the HTML5 spec for giving distinct meanings to “run”
and “execute”.) Parser-inserted scripts are run when the
parser processes the </script>
end tag. Script-inserted scripts are run when the script
element node is inserted into a document.
The act of passing the script text to the JavaScript engine and asking the JavaScript engine to evaluate the text as JavaScript.
The old Firefox behavior was to execute non-async, non-defer scripts in the order in which they were run. (See above for the definitions of “execute” and “run”.) This may seem logical, but it caused some problems, because it meant that:
Script-inserted inline scripts didn’t execute synchronously if there were external scripts being fetched. This caused undesirable effects when inserting an inline script in order to evaluate some JavaScript in the global scope and there were pending external script fetches. (E.g. jQuery uses this method of evaluating JavaScript text in the global scope.) In IE and WebKit, this trick for evaluating some text as JavaScript in the global scope would always evaluate the script text synchronously.
Script-inserted scripts could block the parser upon the next parser-inserted script. This meant that sites that tried to use script-inserted external scripts instead of parser-inserted external scripts as a performance trick to overlap script fetching and parsing wouldn’t get the performance benefit they got in IE and WebKit if there was a subsequent parser-inserted script in their document.
Script-inserted external non-async, non-defer scripts executed in the order they were inserted into the document.
The last point is actually desirable in some cases.
Opera behaves like Firefox 3.6 except script-inserted external scripts block the parser even if there are no subsequent parser-inserted scripts.
In IE and in Safari releases (but no longer in WebKit nightlies), the behavior for script-inserted scripts is as follows:
Script-inserted inline scripts are executed synchronously upon insertion.
Script-inserted external scripts are executed as soon as the script file has been fetched. This means that script-inserted external scripts can execute in any order depending on network conditions.
If a script inserts an external script with a bogus MIME type
in the type
attribute, the external
file is fetched and the onload
event is
fired for it, but the file isn’t executed as a script.
HTML5 standardizes the intersection of the guarantees provided by the legacy versions of the top four browser engines. That is, the HTML5 behavior is like the old IE/WebKit behavior except scripts with bogus types aren’t fetched.
Specifically, the HTML5 behavior is as follows when the interaction with pending style sheet loads is omitted from discussion for simplicity:
Defer scripts (which can only be external and parser-inserted per terminology above) execute when the parser has processed the entire input stream. The defer scripts execute in the order in which they appeared in the input stream.
Parser-inserted async scripts and script-inserted external scripts don’t block anything and aren’t blocked by anything. They execute as soon as the script file has been fetched.
Script-inserted inline scripts execute syncronously when inserted. As a result, they don’t block on anything and can’t be blocked by anything.
Script-inserted external scripts execute as soon as they become available from the network by default.
Script-inserted external scripts whose .async
DOM property returns false
execute in the insertion order relative to other such scripts. (The default is true
. Hence, the default behavior mentioned in the previous bullet point.)
Each parser-inserted non-async, non-defer scripts blocks the parser until they have been executed. Since the parser doesn’t have a chance to insert the next script until the previous one is done, it follows that parser-inserted non-async, non-defer scripts execute in the order their end tags appear in the input stream.
This is fine, because it is the simplest and the most performant solution that is compatible with existing sites that are compatible with existing browsers without browser sniffing. After all, cross-browser-compatible sites (that don’t sniff) can’t rely on characteristics of script loading that aren’t provided by all browsers.
Firefox 4 implements the HTML5
behavior. The main reason for making the change now in Firefox was that the interaction of the old Firefox behavior and HTML5-compliant document.write()
was problematic.
So the HTML5 behavior works for existing sites that use Same Markup (to use Microsoft’s term) for all browsers. The problem is that some sites don’t. That is, sites that sniff for Firefox/Gecko (and Opera) can break.
So far, I’ve seen three classes of sniffing:
Sniffing for Firefox/Gecko and Opera and running HTML5-incompatible code in Firefox and Opera and cross-browser / HTML5-compatible code otherwise.
Sniffing for IE and WebKit and running and cross-browser / HTML5-compatible code in IE and WebKit and running HTML5-incomptible code otherwise.
Sniffing for Firefox/Gecko and Opera and running HTML5-incompatible code in Firefox and Opera and differently HTML5-incompatible code that works in IE and old WebKit otherwise. (Seen in the LABjs library (in versions earlier than 1.0.4) and in the “order” plug-in for the RequireJS library in (in versions earlier that 0.15.0). LABjs 1.0.4 and RequireJS 0.15.0 has been updated to support Firefox nightlies.)
Note that the third kind of sniffing breaks in WebKit nightlies, too.
Use document.write("\u003Cscript src='foo.js'>\u003C/script>");
. This solution is cross-browser-compatible without sniffing. If you write multiple scripts at once, Firefox 4 downloads the scripts in parallel, so concern about parallel downloads is no longer a reason to avoid document.write
in Firefox.
Alternatively, you could use plain <script>
tags in the HTML source transferred over HTTP. This way Firefox (and other browsers) that implement speculative script loading will start fetching the scripts even earlier and will load the scripts in parallel.
Set an onload
event handler on the external script and insert the inline script from the event handler. This solution is cross-browser-compatible without sniffing.
onload
has fired) and want them to execute in a certain order…In Firefox 4, in compliance with the latest HTML5 drafts, the .async
DOM property returns true
for
script-inserted scripts by default. However, script-inserted external scripts whose .async
DOM property returns false
execute in the
insertion order relative to other such scripts.
Thus, you can feature-detect if document.createElement("script").async
evaluates to true
and assume that if it does, setting .async=false
on multiple script-inserted external scripts makes those scripts execute in the insertion order.
To make your code work robustly and as performantly as possible in both Firefox 4 and Firefox 3.6, I encourage you not only to set .async=false
for script-inserted external scripts that you want to execute in the insertion order but also to explicitly set .async=true
on script-inserted external scripts that don’t have ordering dependencies.
Unfortunately, this solution won’t work cross-browser until other browser implement this behavior, so for the time being, if document.createElement("script").async
does not evaluate to true
, you need to fall back onto whatever sniffing-based code you had (which probably fails with WebKit trunk) or onto fetching scripts one by one.
Note: Be sure to have the latest beta or nightly for testing your code.