The DOMContentLoaded event is one of the main pillars of modern, unobtrusive JavaScript usage. This event fires after the HTML code has been fully retrieved from the server, the complete DOM tree has been created and scripts have access to all elements via the DOM API. Usually, this point in time is called “DOM ready”. In contrast to the load
event, assets like images, iframes and plugins do not defer the DOMContentLoaded event. Thus, the DOMContentLoaded is ideal for authors attaching their JavaScript behavior to the page as early as possible.
Nearly all JavaScript frameworks allow authors to start-up their scripts “on DOM ready”. It’s a common JavaScript programming pattern to put all your code in a private function scope and load this function “on DOM ready”. The code in such a start-up function queries the DOM, usually with the aid of selector engines. It gets information from the DOM, binds event listeners to elements, modifies the DOM etc. As I pointed out, this pattern is essential for unobtrusive DOM Scripting.
How to make use of DOMContentLoaded in some popular frameworks:
jQuery(document).ready(function ($) { ... });
or just jQuery(function ($) { ... });
document.observe('dom:loaded', function () { ... });
window.addEvent('domready', function () { ... });
Microsoft Internet Explorer doesn’t support DOMContentLoaded up to and including version 8, so these frameworks make use of the doScroll() workaround from Diego Perini.
To describe the exact behavior of the DOMContentLoaded event in my JavaScript tutorials, I’ve conducted several tests. The main question is: Does the loading of external stylesheets defer the DOMContentLoaded event? This issue isn’t new, it has already been investigated several years ago.
For the most scripts, it makes sense to execute start-up scripts after all stylesheets have been loaded because these scripts rely on the fact that CSS rules have already been applied to the DOM. For the initialization of some scripts, it’s crucial to get the dimensions or the position of an element, for example.
The initial definition of Mozilla says that DOMContentLoaded fires after the document has been parsed and the scripts therein have been executed. But in practise, DOMContentLoaded sometimes take stylesheets into account depending on how scripts are placed in the document and which type they are.
DOMContentLoaded doesn’t wait for stylesheets to load provided that no scripts are placed after the stylesheet reference, . This is valid for all browsers supporting DOMContentLoaded.
Testcase #1: no scripts after stylesheet
There are several exceptions in which Gecko and Webkit do wait for the stylesheet to load before DOMContentLoaded fires. The most common case is being followed by an external script,
. The script can be placed anywhere in the document as long as it’s after the
element.
A minimal testcase looks like this:
The element
stylesheet.css:
#element { color: red; }
script.js:
document.addEventListener('DOMContentLoaded', function () { // should read #FF0000 or rgb(255, 0, 0) alert(getComputedStyle(document.getElementById('element'), null).color); }, false);
Testcase #2: external script after stylesheet
To demonstrate the browser differences, I’m forcing the HTTP server to serve the stylesheet with a delay of three seconds so the document parsing can finish before the stylesheet is received.
The code above works as expected in Firefox, Safari and Chrome, but fails in Opera.
Placing external scripts after the stylesheets has become a common practice. The jQuery documentation recommends this very element order if you want to access a fully styled document in your DOM ready handler. Even if the stylesheet takes, say, 10 seconds to load and the document is received and parsed after one second, DOMContentLoaded doesn’t fire before the stylesheet has arrived.
This involves pros and cons for DOM Scripting. You can count on the stylesheets being applied, but your scripts have to wait quite a time until they can traverse the DOM tree and register event handlers to elements.
These observations are based on browser quirks on a more fundamental level, the HTML parsing and script execution algorithm.
In these browsers, the loading of stylesheets blocks the execution of external scripts. Testcase #2 contains the following markup in the document’s head:
Current Gecko and Webkit versions as well as Internet Explorer 8 download the stylesheet and the script in parallel with multiple HTTP connections. But they don’t execute the script until the stylesheet has been fetched. And they don’t start to render the page in the mean time. You can verify this observation using Firebug’s Network tab, the Resources tab from Safari’s Web Inspector and, most accurately, the Timeline tab in Chrome’s Developer Tools:
Since DOMContentLoaded is fired after all scripts have been executed, the consequence is that DOMContentLoaded will always be fired after the stylesheet has been downloaded and processed.
The same applies to Internet Explorer 8, except for the DOMContentLoaded part, of course.
However, this trick to postpone DOMContentLoaded does not work for Opera. Opera executes the script as soon as it’s fetched and goes on with parsing immediately. This leads to incremental rendering, which is good for perceived performance, but also to a flash of unstyled content (FOUC), which is rather undesirable.
To normalize Opera’s deviation, jQuery 1.2.1 to 1.2.6 performed an additional check after the DOMContentLoaded event. In these jQuery versions, it was guaranteed that all stylesheets were loaded before the DOM ready handlers were called. jQuery 1.3 dropped this workaround. (Prototype and Mootools do not fix and didn't ever try to fix this issue, as far as I know.)
Inline scripts also cause different browser behavior.
Testcase #3: inline scripts after stylesheet
In Internet Explorer and Gecko, a stylesheet also blocks the execution of subsequent inline scripts. Consequently, DOMContentLoaded is delayed.
In Webkit and Opera, the inline script is executed immediately. Hence, DOMContentLoaded will fire as soon as the HTML is parsed regardless of the stylesheet.
Browser Engine vs. Behavior |
Stylesheets delay DOMContentLoaded if there are only scripts before the stylesheet link Testcase #1 |
Stylesheets block the execution of subsequent external scripts and thereby delay DOMContentLoaded Testcase #2 |
Stylesheets block the execution of subsequent inline scripts execution and thereby delay DOMContentLoaded Testcase #3 |
---|---|---|---|
Presto (Opera) | ☐ No | ☐ No | ☐ No |
Webkit (Safari, Chrome) | ☐ No | ☒ Yes | ☐ No |
Gecko (Firefox) | ☐ No | ☒ Yes | ☒ Yes |
Trident (Internet Explorer) | n/a | ☒ Yes | ☒ Yes |
Once, DOMContentLoaded was a Mozilla invention for internal use by Firefox addons. It didn’t take long for the evolving Unobtrusive JavaScript community to figure out how useful this event is for general Web sites. That’s why Opera and Apple adopted this event. But their HTML parsing implementations differ, as my tests revealed, and so therefore DOMContentLoaded has a different meaning.
Fortunately, the most important webstandard to-be, HTML5, is going to codify DOMContentLoaded and the exact HTML parsing process.
According to HTML5, DOMContentLoaded is a plain DOM ready event without taking stylesheets into account. However, the HTML5 parsing algorithm require browsers to defer the execution of scripts until all previous stylesheets are loaded. Let’s get back to the example from testcase #2:
When the HTML5 parser encounters the tags, the whole parsing process is halted. First, the browser fetches the script resource, given it’s an external script. Second, the browser waits for the preceding stylesheet to load. Third, the JavaScript engine runs the downloaded script code. Finally, the parser continues to parse the HTML document.
Given that at least some scripts are positioned after the stylesheets in the HTML source code, it is guaranteed that all stylesheets have been loaded at the moment of “DOM ready”. That’s because DOMContentLoaded
fires after the whole document has been parsed.
It turns out that Gecko and Internet Explorer already conform to HTML5 in this regard. But we don’t know yet when Webkit and Opera will switch to HTML5 parsing and if a HTML5 doctype will be necessary to activate the HTML5 parser.
As said above, this parsing rule has a huge performance impact. If we put all stylesheets and scripts in the document’s head
, the parsers waits for them to download until it continues to parse the body
. To achieve a better perceived performance and incremental page rendering across browsers, the basic rule is “put stylesheets at the top, but scripts at the bottom” (see High Performance Web Sites by Steve Souders).
This is the detailed HTML5 parsing process regarding external scripts (without defer
or async
attributes):
end tag is encountered.An end tag whose tag name is "script"
Then, the first of the following options that describes the situation must be followed:
If the element has a src attribute, and the element has been flagged as "parser-inserted", and the element does not have an async attribute:
At this stage, if there is a pending parsing-blocking script, then:
Otherwise:
Wait until there is no style sheet blocking scripts and the script's "ready to be parser-executed" flag is set.
Step 7. Execute the script.
For internal scripts, it’s eventually the same because script execution always includes the step Pause until either any applicable style sheets have been fetched and applied, or the user agent has timed out and decided to not wait for those style sheets.
Thanks to Mozilla Developer Boris Zbarsky who helped me to understand to HTML5 parsing algorithm on the WHATWG mailing list.
Update, 2010-02-15: Few hours after I had posted this description, the HTML5 specification was changed. The crucial section Pause until either any applicable style sheets have been fetched and applied
was removed. But as far as I can see, this revision does not change the parsing behavior with regard to normal internal and external scripts. I’m going to update the parsing process description soon.