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のスコアも上がります。

Render-Blocking Resources  |  Tools for Web Developers  |  Google Developers
Reference documentation for the "Render-blocking stylesheets" and "Render-blocking scripts" Lighthouse audits.

タイミングによってはイベントが発生しない

一見、async属性を追加するだけで高速化ができ、PageSpeed Insightsのスコアが上がるのであれば手当たり次第にasync属性を追加したくなると思いますが、ちょっと待ってください。

非同期になるということはそのJavaScriptがいつ実行されるかまったく保証されません。レタリングの途中で実行されるかもしれないし、レタリングが終わったあとに実行されるかもしれないのです。

もし、レタリングが終わったあとに実行された場合、比較的よく使用されるイベントである「DOMContentLoaded」イベントが発生しないのです。

現象の再現

次のサンプルページで現象を再現できます。ただし、回線の速さ・PCの処理の速さによっては正常に読み込めてしまう可能性があります。

デフォルトの場合
asyncが有効な場合
スポンサーリンク

対処方法

上記ページを見て察し頂けたかと思いますが、対処方法はいくつかあります。

1. 「load」関数に代替えする

DOMContentLoaded」イベントを使用せずに「load」関数を使用する方法です。「load」関数はすべての準備(JavaScriptや画像の読み込みなど)が終わってから実行されます。しかし、準備を待つために場合によっては実行タイミングがとても遅いため注意が必要です。

2. すでにレタリングされている場合は即時に実行する

レタリング完了後は、document.readyStatecompleteまたはinteractiveとなっているため、次のようなif文で、loading以外であれば即時実行するようにプログラムを変更します。

if (document.readyState !== 'loading') {
  eventHandler();
} else {
  document.addEventListener('DOMContentLoaded', eventHandler, false);
}

Document.readyState
Document.readyState プロパティは、その文書の読み込み状態を示します。

3. jQueryを使用する

jQueryの$(function(){}では、async属性であっても、レタリング中であれば「DOMContentLoaded」のタイミングで実行、レタリングが終わっていれば即時実行されます。内部的には2項の方法で対策しているようです。

※当然ながらjQueryライブラリがイベントプログラムよりも先に読まれないと正常に動作しません(jQueryライブラリはasyncで読み込めません)。

コメント