HTMLに書いたJavaScript実行のタイミングを改めて見返していたら、いろいろと発見がありました。
TL; DR
-
DOMContentLoaded
はパーサーのイベント -
<script>
要素には、表に現れない内部属性がある - タイマーで
DOMContentLoaded
より前に割り込める
DOMContentLoaded
は何のイベント?
よくJavaScriptの実行タイミングとして使われるDOMContentLoaded
ですが、厳密に言えば何が起きているかをご存知でしょうか。実は…
Fired at the
Document
once the parser has finished (HTML LSより)
と、HTMLのパーサーが完了したタイミングで起きるイベントなのです。
パーサーとJavaScript
「パースだけならすぐ終わってしまうんじゃない?」というのが正直な感想かと思いますが、HTMLのパーサーは単純にHTMLだけの処理をしているわけではありません。
さらに厳密な話は後からしていきますが、文書の途中に書いた<script>
がそれ以前のDOMだけ読める、というように、HTML内にあるJavaScriptは、HTMLのパースを中断することがあります。
もっと強烈なものとして、document.write()
があります。これはHTMLとして出来上がっていない断片(開きタグだけとか、コメントに入る<!--
だけとか)すら出力できるというように、document.write()
の結果は文書全体のパーサに影響することとなります(このため、<script>
以降のパースは、JavaScriptを実行し終えてからでないと行えません)。
<script>
要素の内部属性
HTMLやJavaScript処理の過程で、<script>
にはいくつかのフラグが付けられます。
-
already started
…実行済みかどうか -
parser-inserted
…Document
のパーサーが生成した(HTMLに書いてあった、もしくはdocument.write
で出力した)か -
non-blocking
…スクリプトのロードがドキュメントをブロックしないか(基本的に、parser-inserted
の場合にfalseとなります)。- なお、
async
とは別管理となっていて、(あとからasync
を付け外ししたときに実行漏れになるのを防ぐためなのか、理由ははっきりわかりませんが)async
を付けたスクリプトに対してはnon-blocking
はfalseとなります。
- なお、
なお、以下の解説ではできるだけふつうのHTMLに即した形にしてありますが、動的に<script>
要素を作り変えた場合など、特殊な場合にはこれらのフラグで考える必要があります。
状況に応じた挙動
<script>
要素の状況に応じて、それがどのように実行されるか区分していきます。なお、煩雑になるのを避けるため、type="module"
の場合の挙動については省略します。
HTMLに書いてあった、あるいはdocument.write()
で生成した場合
-
src
あり、defer
あり、async
なしの場合…パーサーの完了時に、HTMLにある順序に従って実行される(実行が終わるまでパーサーは完了しません) -
src
あり、defer
なし、async
なしの場合…JavaScriptのロード完了までパーサーを止めて、その場で実行します。 ※ -
src
あり、async
ありの場合…読み込みが終わり次第、文書での順番に関係なく実行される。 -
src
なし(直書き)の場合…パーサーを止めて、その場で実行します。 ※
※: 「その場で実行する」ものの場合、これ以前に読み込まれたスタイルシートがあると、その読み込みを待つこととなります。本来、スタイルシートの読み込みはパーサーをブロックしない(DOMContentLoaded
に影響しない(MDN))のですが、同期的に実行されるスクリプトが1つでもあると、「スクリプトがパーサーをブロックする」→「スタイルシートがスクリプトをブロックする(HTML LS)」の2段構成で、スタイルシートもDOMContentLoaded
前のロードが強制される形となります。
DOMで作った場合
もちろん、DOMと言ってもdocument.write()
は除きます。
-
src
ありの場合…読み込みが終わり次第、文書での順番に関係なく実行される(async
がある場合と同じ)。 -
src
なしの場合…ドキュメントツリーへの挿入と同時に、同期的に実行される。
DOMContentLoaded
前に処理を割り込ませる
defer
となったスクリプトがある場合、それが読み込まれるまでDOMContentLoaded
が発生しませんので、ファーストビューがDOMContentLoaded
前に発生することもあります。そのような状況で「仮想DOMを出来るだけ早くマウントしたい」という場合に、挿入する場所の直後、あるいは</body>
直前に<script>
を書くことで、そのタイミングで実行させるという方法があります。
ページの上に入れた場合にも、タイマー系はDOMContentLoaded
と関係なく動作しますので、requestAnimationFrame
でループさせて、目的のエレメントを検出する、なんていうことをやってみてもいいかもしれません。