DOMContentLoaded and stylesheets · molily

Introduction to DOMContentLoaded

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:
jQuery(document).ready(function ($) { ... }); or just jQuery(function ($) { ... });
Prototype:
document.observe('dom:loaded', function () { ... });
Mootools:
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.

DOMContentLoaded and external stylesheets

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.

Delaying DOMContentLoaded in Gecko and Webkit with an external script

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.

Background: HTML parsing

These observations are based on browser quirks on a more fundamental level, the HTML parsing and script execution algorithm.

Stylesheets block execution of external scripts in Gecko, Webkit and IE

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.

Stylesheets do not block execution of external scripts in Opera

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.)

Stylesheets block execution of internal scripts in Gecko and IE

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.

Summary

DOMContentLoaded and stylesheets overview
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

HTML5 to the rescue? Yes! (updated on February 14)

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).

Appendix

This is the detailed HTML5 parsing process regarding external scripts (without defer or async attributes):

  1. When in head or in body, the tokenizer emits a a end tag is encountered.
  2. Case An end tag whose tag name is "script"
  3. Run the script
    1. Step 7. Fetch the script if it’s external
    2. Step 8. Then, the first of the following options that describes the situation must be followed:
    3. Case 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:
    4. The element is the pending parsing-blocking script
    5. Set the "ready to be parser-executed" when fetched
  4. Back to tree construction
  5. At this stage, if there is a pending parsing-blocking script, then:
  6. Case Otherwise:
  7. Step 3. Wait until there is no style sheet blocking scripts and the script's "ready to be parser-executed" flag is set.
  8. Step 7. Execute the script.

    1. Step 2. 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.
    2. Step 3. Execute the JavaScript code synchronously

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.