async属性で読むと「DOMContentLoaded」イベントが発生しない理由
はじめに
async で JavaScript を読むと DOMContentLoaded イベントが発生しません。 async 属性で読み込みつつもイベントを発生させる方法を紹介します。
async 属性とは
通常、ブラウザは読み込みの途中で <script> タグがあるとそのスクリプトを実行し終わるまで一時的にレンダリングを中断します。JavaScript の実行タイミングを保証するためです。同期処理とも呼ばれます。
<script src="javascript.js"></script>
しかし、src 属性を持つ <script> タグに async 属性を付与することにより、レンダリングが中断されなくなります。JavaScript の読込・実行とページのレンダリングが並行して行われるようになり、実行タイミングが保証されなくなります。非同期処理と呼ばれます。
<script src="javascript.js" async></script>
非同期になることにより、レンダリングが中断されずにページ表示の高速化が期待できるため、Google などが推奨しています。PageSpeed Insights のスコアも上がります。
Eliminate render-blocking resources - web.dev
https://web.dev/render-blocking-resources/
タイミングによってはイベントが発生しない
一見、 async 属性を追加するだけで高速化ができ、PageSpeed Insights のスコアが上がるのであれば手当たり次第に async 属性を追加したくなりますが、そううまくはいきません。
非同期になるということはその JavaScript がいつ実行されるかまったく保証されません。レンダリングの途中で実行される可能性もあり、レンダリングが終わったあとに実行される可能性もあります。
もし、レンダリングが終わったあとに実行された場合、比較的よく使用されるイベントである DOMContentLoaded イベントが発生しないのです。

現象の再現
次のサンプルページで現象を再現できます。ただし、回線の速さ・PC の処理の速さによっては正常に読み込めてしまう可能性があります。
対処方法
対処方法はいくつかあります。
1. load イベントに変更する
DOMContentLoaded イベントを使用せずに load イベントを使用する方法です。 load イベントはすべての準備(JavaScript や画像の読み込みなど)が終わってから実行されます。しかし、準備を待つために場合によっては実行タイミングはとても遅いので注意が必要です。
Window: load イベント - Web API | MDN
https://developer.mozilla.org/ja/docs/Web/API/Window/load_event
window.addEventListener("load", function () {
// ...
});
2. すでにレンダリングされている場合は即時に実行する
レンダリング完了後は、 document.readyState が complete または interactive となっているため、次のような if 文で、 loading 以外であれば即時実行するようにプログラムを変更します。
if (document.readyState !== "loading") {
eventHandler();
} else {
document.addEventListener("DOMContentLoaded", eventHandler, false);
}
Document.readyState - Web API | MDN
https://developer.mozilla.org/ja/docs/Web/API/Document/readyState
3. jQuery を使用する
jQuery の $(function(){} では、async 属性であっても、レンダリング中であれば DOMContentLoaded のタイミングで実行、レンダリングが終わっていれば 即時実行されます。内部的には 2 項の方法で対策しているようです。
※当然ながら jQuery ライブラリがイベントプログラムよりも先に読まれないと正常に動作しません(jQuery ライブラリは async で読み込めません)。
$(function () {
$("#jquery").text("発火!🔥");
});