DOMContentLoaded and stylesheets

· Keine Kommentare

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.

It’s debatable when DOMContentLoaded should fire and whether it should wait for stylesheet to load. 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. That’s also plausible since the main purpose of DOMContentLoaded is to provide an event for the “DOM ready” moment. After all, it’s called DOMContentLoaded and not “DOMContentAndStylesheetsLoaded”.

Nevertheless, the browser vendors realized how DOMContentLoaded is actually used and moved to a heuristic approach which sometimes take stylesheets into account. The current state of knowledge is: stylesheets delay DOMContentLoaded in Gecko (Firefox) and WebKit (Safari, Chrome), but not in Presto (Opera). My research has shown that the answer isn’t that simple, it rather depends 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, <link rel="stylesheet">.

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 <link rel="stylesheet"> being followed by an external script, <script src=""></script>. The script can be placed anywhere in the document as long as it’s after the <link> element.

A minimal testcase looks like this:

<!DOCTYPE html>
<head>
<link rel="stylesheet" href="stylesheet.css">
<script src="script.js"></script>
</head>
<body>
<div id="element">The element</div>
</body>

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

I’ve come to the conclusion that 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 …

The explanation I’ve found is the fact that the loading of external stylesheets blocks the execution of external scripts. Testcase #2 contains the following markup in the document’s head:

<link rel="stylesheet" href="stylesheet.css">
<script src="script.js"></script>

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.

… but not 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 Mootols 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.

<link rel="stylesheet" href="stylesheet.css">
<script> Some inline JavaScript code </script>

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 and behaves like a (non-existant) “DOMContentAndStylesheetsLoaded”.

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

  • Opera is the only browser in which DOMContentLoaded means DOM ready without stylesheet in either case. That’s not what script authors always want, but at least it’s clear, consistent and predictable.
  • Webkit and Gecko wait for a stylesheet to arrive if it’s followed by an external script.
  • In addition to this, Gecko defers DOMContentLoaded if there’s an inline script after the stylesheet link. My assumption is that is behavior is adopted from Internet Explorer. Even though IE doesn’t support DOMContentLoaded, it handles script execution in the same way.
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?

DOMContentLoaded was once 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 implementations differ, as my tests revealed.

Fortunately, the most important webstandard to-be, HTML5, is going to codify DOMContentLoaded and the exact HTML parsing process. That’s fine and adheres to the virtue “Paving the cowpaths”.

Unfortunately, the normative specification will conflict with another HTML5 principle, “Don’t break the Web”. That’s because HTML5 opts for the original definition of DOMContentLoaded like it’s implemented in Opera. It’s a plain DOM ready event without taking stylesheets into account.

In addition to this, the HTML5 parsing algorithm does not require browsers to defer the execution of subsequent scripts. That’s my current understanding of the HTML5 specification, I’m possibly mistaken.

This will break compatibility with the three biggest browser engines, Gecko, Webkit and Internet Explorer. We’re faced with a dilemma: It’s necessary to standardize DOMContentLoaded and there are good points for a simple DOM ready definition which ignores stylesheets. But the question is, will Gecko and Webkit change their behavior in order to conform to HTML5? I guess they can’t without any difficulty, because thousands of sites rely upon the current implementation. More sites than those who rely upon Opera’s behavior since Opera has a smaller market share.

A possible solution would be the introduction of another event like “DOMContentAndStylesheetsLoaded” which fires after HTML parsing, script execution and stylesheet processing.