このエントリー
DOMContentLoaded周りの話は、パフォーマンス改善の文脈で出てくることがよくありますが、根本のHTMLの仕様に踏み込んだ記事は少なく個人的に引っかかっていました。そのため、周辺で知っておいたほうがよさそうな内容をまとめてみました。
もし、違うよとかここにリファレンスあるよとかあれば、ぜひ教えていただけると助かります。
1.DOMContentLoadedの策定
2.$(document).readyとの違い
3.DOMContentLoadedが発火するまで
1. DOMContentLoadedの策定
DOMContentLoadedはHTML5に準拠した仕様です。
HTML5は
- 2004年に発表されたWeb Applications 1.0に端を発し
- 2008年にドラフト公表
- 2014年に勧告
と約10年!!かけて勧告まで辿りつく状態でしたが、DOMContentLoadedの有用性は早期から認知されており、一部のブラウザでは早い段階からサポートされてきました。
ブラウザ | Version | リリース |
---|---|---|
Firefox | 2 | 2006年10月24日 |
Safari | 3.1 | 2008年03月18日 |
Google Chrome | 4 | 2010年01月25日 |
Internet Explorer | 9 | 2011年03月15日 |
利用者が一定数いるIE6,7,8を除いて、現在利用されているほぼ全てのブラウザで標準仕様として扱えるようになっています。2006年ごろから登場したjQueryの$(document).readyは有名ですが、この存在もDOMContentLoadedが正式に取り入れられるきっかけの1つになっています。
2. jQueryのreadyとの違い
HTML5で定義されているDOMContentLoadedとjQueryの$(document).readyは、DOMツリーの構築が完了したことを判定するという点で役割は一緒といえます。ただ、jQueryは古いバージョンのブラウザをサポートするためにDOMContentLoadedを使わないready判定の実装を行っていました。
1. jQuery Core 1.0のready判定(1.0から1.2.1まで)
jQyer Core 1.0を見るとIE/Safari向けに以下の実装を行っています。
// If IE is used, use the excellent hack by Matthias Miller
// http://www.outofhanwell.com/blog/index.php?title=the_window_onload_problem_revisited
} else if ( jQuery.browser.msie ) {
// Only works if you document.write() it
document.write("<scr" + "ipt id=__ie_init defer=true " +
"src=//:><\/script>");
// Use the defer script hack
var script = document.getElementById("__ie_init");
script.onreadystatechange = function() {
if ( this.readyState == "complete" )
jQuery.ready();
};
// Clear from memory
script = null;
// If Safari is used
} else if ( jQuery.browser.safari ) {
// Continually check to see if the document.readyState is valid
jQuery.safariTimer = setInterval(function(){
// loaded and complete are both valid states
if ( document.readyState == "loaded" ||
document.readyState == "complete" ) {
// If either one are found, remove the timer
clearInterval( jQuery.safariTimer );
jQuery.safariTimer = null;
// and execute any waiting functions
jQuery.ready();
}
}, 10);
}
IEに対してはscriptにdefer属性をつけると、DOMツリーの構築後にscriptの処理が行われることを利用し、defer属性をつけたscriptを生成し、そのscriptの実行が完了できているかどうかを判定することでdocumentのready判定を行っています。
Safariは当時からreadyStateをサポートしていたのでそちらを利用していたようです。この仕様はjQuery Core 1.2.1まで引き継がれています。
詳細の仕組み:IEのDeferTrickについての詳細(英語)
2. doScrollメソッドでのready判定 (1.2.2から1.11.3まで)
jQuery Core 1.2.2ではready判定の実装が変更され、「doScrollメソッドだけはDOM構築後,loadイベント前に実行できる」という
性質を利用したready判定に変更されました。
if ((jQuery.browser.msie && window == top) || jQuery.browser.safari)(function() {
try {
// If IE is used, use the trick by Diego Perini
// http://javascript.nwbox.com/IEContentLoaded/
if (jQuery.browser.msie || document.readyState != "loaded" && document.readyState != "complete")
document.documentElement.doScroll("left");
} catch (error) {
setTimeout(arguments.callee, 0);
return;
}
doScrollでready判定する実装は、1系の最新バージョンであるjQuery Core 1.11.3まで引き継がれています。
ちなみにjQuery 2系はブラウザサポートとしてIE8以下は対応しないとしているので
こういった独自の実装は見受けられないため
DOMContentLoaded = $(document).readyと捉えておいてよいように思います。
3.DOMContentLoadedが発火するまで
DOMContentLoaded自体は「DOMの構築が完了したら発火するイベント」という内容なので、これ自体は、それほど特筆する必要はありません。ただし、DOMContentLoadedが発火するまでにどういった処理が行われていて、何がDOMContentLoadedの発火を早める(もしくは遅くする)要因なのかは知っておいて損はなさそうなので、HTML5の原文や他の記事等からまとめております。
1. scriptはHTMLパーサをBlockする
HTMLパーサという言葉を聞いたことがあるかもしれませんが、HTMLパーサはHTMLの構文解析とDOMツリーの構築を行う役割を担っています。あくまでDOMツリーの構築がアウトプットなので、描画を行うレンダーツリーの構築は行っていません。DOMContentLoadedは、このHTMLパーサの処理の中で発火されるようになっています。
処理の流れとしては大枠はシンプルで
- Tokenizer(字句解析器)で字句解析を行うためにバイト列をUnicodeの文字集合にデコードする
- Tokenizer(字句解析器)で文字列を解析し、開始タグ/終了タグ/属性/属性値を識別
- TreeConstructionでDOM Treeを構築していき
- DOM(Document)ができあがる
といったことをやっているんですが、document.write()があるとDOMツリーを作り続ける意味がないため、ループで処理をするように設計されています。
これはつまり
- scriptはdocument.write()を持っている可能性がある
- document.write()があるのに解析を続けても意味がない
- scriptタグがあった場合、HTMLパースを止めてしまうべきだ
- だから、スクリプト実行する際にはHTMLパースがブロックされる
というところにつながっているようです。document.write()は普段あまり私自身は利用しないのであれなんですが、設計の根底にはdocument.write()への配慮がありました。
参照:HTML5 Syntax
2. stylesheetはscriptをブロックする
次にstylesheetはscriptをブロックするという話です。
HTML5本家には以下のような記述があります。
If the parser's Document has a style sheet that is blocking scripts or the script's "ready to be parser-executed" flag is not set: spin the event loop until the parser's Document has no style sheet that is blocking scripts and the script's "ready to be parser-executed" flag is set.
意訳:scriptをブロックするstylesheetもしくは未実行のscriptがある場合、ループ処理を行います。
HTMLパースを止める要素としてはscriptがあることを先程触れていますが、もう1つの要素として、間接的にstylesheetもHTMLパースを止める要素となっています。
これは
- script自体がstylesheetを参照して何かしらやることがある
- stylesheetが解析できない状態でscript実行すると思った以上に困ることあるよ
- だったら、syltesheetの解析が済むまではscriptの実行もやらないようにしよう
という背景があるからですが、このことによって、
- HTMLパースはscriptの実行を待つ
- sciprtはstylesheetの解析を待つ
という順番待ちの流れができています。
たまに「stylesheetをscriptより先に持ってきたほうがいい」という話を見かけることがありますが、その出元はここらへんの仕様に基づいているようです。もちろん、ブラウザの並列処理はかなり進化してるので、レスポンス/ファイル容量の組み合わせで必ずしもそうとはいえない状態も出てきているようですが、DOMツリーができるまでには、こんなことが行われています。
3. ブロッキングを解決するためのdefer属性/async属性
では、これらのブロッキングを回避する手段を用意しないとね、として生まれたのがdefer属性とasync属性です。deferはHTML4.01から、asyncはHTML5から仕様として策定されています。
イメージとして非常にわかりやすいのが、こちらです。deferはDOMContentLoadedの直前、asyncはそれとは関係なしに実行されることがわかります。
もちろんこれはあくまでざっくりイメージなので詳細を追うと、もう少し細かくなってこういった形になります。
属性 | 詳細 |
---|---|
defer |
・ダウンロードは即座に行われる。 ・DOMの解析が終了してから実行される。 ・readyStateが"interactive"になった直後にscript実行 ・DOMContentLoadedはdeferのscriptが実行された後に発火 |
async |
・ダウンロードは即座に行われる。 ・DOMの解析スレッドと別のスレッドで処理される。 - そのため、stylesheetのscriptのブロッキングを受けない。 - そのため、asyncのscriptを回避してDOMの解析と構築は完了する。 - ただし、DOM構築が完了する前にscriptが実行された場合は、HTMLパースを止めることがある。 ・window.loadが終わるまでに実行される。 |
「asyncは非同期だよ!!」としか説明されていないことが多いので、あまり意識することもなかったのですが、asyncにするとstylesheetのscriptのブロッキングを回避できるとか、asyncであってもDOMに影響がある場合、HTMLパースは止まってしまうであったりは覚えておくと便利なように思いました。
なお、deferとasyncを両方つけることはできて、通常のブラウザはasyncとして実行され、未対応のブラウザはdeferと解釈して実行されるようなので、defer/asyncを両方つけておくと盤石かと思われます。
4.関連記事
ここからは関連記事を紹介します。
- HTML5の仕様(日本語訳が追いついていないので本家を参照しています)
- ブラウザの仕組み
- ビジュアルでわかるdefer/asyncの違い
- defer/asyncの使い方
- 高速化
- jQuery
5.所感
HTML5の仕様を日本語で探しても訳がなくて困りましたが、原文なんとか読みながらまとめてみました。読んでみた方はご存知かもしれませんが、ブラウザ開発者向けに書かれているんだろうなぁというぐらい処理が細かく書かれているので、大枠を掴むのが大変でした。ちょっとだけブラウザ開発者の気持ちもわかったので、これからはブラウザにも優しくなれそうです。