[whatwg] scripts, defer, document.write and DOMContentLoaded

Hi all!

I wanted to share some experiences with implementing the defer
attribute on scripts.

After the initial implementation[1] one of the problems that we
quickly ran in to was pages going blank [2][3][4] due to pages
containing markup like

<script defer>
  document.write("something");
</script>

In IE this will in some circumstances (more below) not replace the
current page, but instead just append the written markup to the end of
the page. There was even one large site that had markup like

<script defer>
  if (navigator.platform == "MacIntel") {
    document.write("something");
  }
</script>

even though as far as I know there is no browser running on mac that
supports defer (maybe Opera?).

What's tricky is that a document.write inside a deferred script in IE
will in some circumstances clear the current document, in other cases
append to it. Specifically it seems that if the page has ever set
.innerHTML on any node then document.write in the deferred script will
append to the current page. If .innerHTML has never been set, then it
will replace the current page.

In order to fix this in Gecko we decided [2] to let the "insertion
point" be set to the end of the document while executing all deferred
scripts. Additionally we hold off firing DOMContentLoaded until all
deferred scripts have loaded and executed. This was in part due to
implementation specific problems (a lot of internal mozilla code
relies DOMContentLoaded), but also to allow pages to more easily defer
scripts across browsers [5]. Additionally always firing
DOMContentLoaded before all, or after all, deferred elements increases
consistency and reduces the risk of race conditions in pages.

Also, in an effort to reduce the amount of possible race conditions in
pages, if there are any scripts that were inserted using the DOM, we
wait for all those to finish loading and executing before firing
DOMContentLoaded. This was mostly because in gecko the concept of
"insertion points" works differently and so document.write never
clears the current document before DOMContentLoaded has fired, making
races with DOMContentLoaded particularly noticeable.

Another thing where it appears that the spec differ from Geckos
behavior is nodes that are not async and not deferred. Gecko always
executes these in the order they are inserted into the main Document.
So if two <script> nodes are created, one with a src attribute set,
and another with just inline script, and then inserted into the
Document, they are always executed in insertion order no matter of the
src-script or the inline-script is inserted first.

All of the above is how things work in Firefox 3.5.

In addition to this we in the next version of firefox where
document.readyState is supported, hold off setting document.readyState
to "interactive" until all deferred scripts have executed. It appears
that the spec changes readyState before executing deferred scripts
which I think would be fine to change our implementation to do.

So all in all, I'd like to see the following changes to the spec:

* Don't fire DOMContentLoaded until all deferred scripts have executed.
* While executing deferred scripts, let the "insertion point" be at
the end of the document.
* Always execute elements in the order they are inserted into the
Document, with exception of async and deferred scripts.
* Possibly hold off firing DOMContentLoaded until any outstanding
scripts have finished loading and executing.

[1] https://bugzilla.mozilla.org/show_bug.cgi?id=28293
[2] https://bugzilla.mozilla.org/show_bug.cgi?id=461555
[3] https://bugzilla.mozilla.org/show_bug.cgi?id=461724
[4] https://bugzilla.mozilla.org/show_bug.cgi?id=469751
[5] https://bugzilla.mozilla.org/show_bug.cgi?id=474392

/ Jonas

Received on Tuesday, 7 July 2009 00:20:56 UTC