async属性を使うと "DOMContentLoaded" イベントが発火しない理由
async
、defer
、type="module"
でJavaScriptを読むと DOMContentLoaded
イベントは発火しないことがあります。これらの属性を使いつつもイベントを発火させる方法を紹介します。
async 属性とは
通常、ブラウザは読み込みの途中で <script>
タグがあるとそのスクリプトを実行し終わるまで一時的にDOM解析を中断します。スクリプトがDOMの要素に依存している場合や、特定の実行順序を必要とする場合があり、JavaScriptの実行タイミングを保証するためです。
<script src="javascript.js"></script>
一方、src属性を持つ <script>
タグに async
属性を付与することにより、DOM解析が中断されなくなります。JavaScriptの読込・実行とページのDOM解析が並行して行われるようになり、実行タイミングが保証されなくなります。
<script src="javascript.js" async></script>
非同期になることにより、DOM解析が中断されずにページ表示の高速化が期待できるため、Web高速化の手法として推奨されている方法です。Lighthouseのスコアも上がります。
タイミングによってはイベントが発火しない
一見、 async
属性を追加するだけで高速化ができ、Lighthouseのスコアが上がるのであれば手当たり次第に async
属性を追加したくなりますが、そううまくはいきません。
asnyc
属性を使用しない場合(<script>
)、JavaScriptのイベントリスナーを登録した後に DOMContentLoaded
イベントが発火することを保証できます。
しかし、async
属性を使用すると、JavaScriptの読み込みと実行が非同期になります。つまり、そのJavaScriptがいつ実行されるかまったく保証されません。DOM解析の途中で実行される可能性もあり、DOM解析が終わったあとに実行される可能性もあります。
もし、DOM解析が終わったあとに実行された場合、DOMContentLoaded
イベントが発火しなくなります。
また、type="module"
や defer
属性を使用した場合もDOM解析が終わったあとに実行されます。
対処方法
対処方法はいくつかあります。
1. すでにDOM解析されている場合は即時に実行する
DOMContentLoaded
イベントだけに頼らず document.readyState
の値も確認して、DOM解析が完了している場合は即時に実行する方法です。
DOM解析完了後は、 document.readyState
が complete
または interactive
となっているため、次のようなif文で、 loading
以外であれば即時実行するようにプログラムを変更します。
function eventHandler() {
document.removeEventListener("DOMContentLoaded", eventHandler);
doSomething();
}
if (document.readyState !== "loading") {
// DOM解析が完了している場合は即実行
doSomething();
} else {
document.addEventListener("DOMContentLoaded", eventHandler);
}
load
イベントに変更する
2. DOMContentLoaded
イベントを使用せずに load
イベントを使用する方法です。 load
イベントはすべての準備(JavaScriptや画像の読み込みなど)が終わってから実行されます。しかし、これらの準備を待つために場合によっては実行タイミングはとても遅いので注意が必要です。
function eventHandler() {
document.removeEventListener("load", eventHandler);
doSomething();
}
window.addEventListener("load", eventHandler);
(おまけ) jQuery における実装
jQueryを活用したとき、 jQuery(function($) {...});
や、 $(function() {...});
をおまじないのように使ったことがあるという方も多いのではないでしょうか。
実は、DOM解析中であれば DOMContentLoaded
のタイミングでイベントハンドラーを実行、DOM解析が終わっていれば即時実行するような仕組みが内部に組み込まれています。
内部的には1、互換性がない場合のフォールバックとして2の方法で実装されています。
まとめ
async
属性を使用すると、JavaScriptの読み込みと実行が非同期になり、DOMContentLoaded
イベントは発火しなくなることがあります。
DOMContentLoaded
イベントが発火しない場合は、 document.readyState
の値を確認して即時実行するか、 load
イベントを使用することで対処できます。
参考文献
詳しい仕様はHTML Living Standardの "4.12.1 The script element" をご参照ください。図解もされていてわかりやすいです。
Discussion
type="module" の場合 top level await が使えるのだから これで良さそう感ある。