現代のウェブサイトでは、スクリプトはHTMLよりも「重い」ことがよくあります。ダウンロードサイズが大きく、処理時間も長くなります。
ブラウザがHTMLを読み込み、<script>...</script>タグに遭遇すると、DOMの構築を続行できません。スクリプトをすぐに実行する必要があります。外部スクリプト<script src="..."></script>についても同様です。ブラウザは、スクリプトのダウンロード、ダウンロードされたスクリプトの実行を待機してから、ページの残りの処理を行う必要があります。
これにより、2つの重要な問題が発生します。
- スクリプトはそれ以下のDOM要素を参照できないため、ハンドラーなどを追加できません。
- ページの先頭に大きなスクリプトがある場合、ページが「ブロック」されます。ユーザーは、スクリプトのダウンロードと実行が完了するまで、ページの内容を参照できません。
<p>...content before script...</p>
<script src="https://javascriptinfo.dokyumento.jp/article/script-async-defer/long.js?speed=1"></script>
<!-- This isn't visible until the script loads -->
<p>...content after script...</p>
これに対するいくつかの回避策があります。たとえば、ページの下部にスクリプトを配置できます。そうすれば、それより上の要素を参照でき、ページの内容の表示がブロックされません。
<body>
...all content is above the script...
<script src="https://javascriptinfo.dokyumento.jp/article/script-async-defer/long.js?speed=1"></script>
</body>
しかし、この解決策は完璧には程遠いです。たとえば、ブラウザは、完全なHTMLドキュメントをダウンロードした後にのみ、スクリプトに気づき(そしてダウンロードを開始できます)。長いHTMLドキュメントの場合、顕著な遅延が発生する可能性があります。
非常に高速な接続を使用している人にとっては目に見えないものですが、世界には依然としてインターネット速度が遅く、完璧とは程遠いモバイルインターネット接続を使用している人が多くいます。
幸いなことに、この問題を解決する2つの<script>属性があります。deferとasyncです。
defer
defer属性は、ブラウザにスクリプトを待たないように指示します。代わりに、ブラウザはHTMLの処理を続け、DOMを構築します。スクリプトは「バックグラウンド」で読み込まれ、DOMが完全に構築されると実行されます。
上記の例と同じですが、deferを使用しています。
<p>...content before script...</p>
<script defer src="https://javascriptinfo.dokyumento.jp/article/script-async-defer/long.js?speed=1"></script>
<!-- visible immediately -->
<p>...content after script...</p>
言い換えると
deferを持つスクリプトは、ページをブロックすることはありません。deferを持つスクリプトは、常にDOMの準備ができたとき(ただし、DOMContentLoadedイベントの前)に実行されます。
次の例は、2番目の部分を示しています。
<p>...content before scripts...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defer!"));
</script>
<script defer src="https://javascriptinfo.dokyumento.jp/article/script-async-defer/long.js?speed=1"></script>
<p>...content after scripts...</p>
- ページの内容はすぐに表示されます。
DOMContentLoadedイベントハンドラーは、遅延スクリプトを待ちます。スクリプトがダウンロードされ、実行された場合にのみトリガーされます。
遅延スクリプトは、通常のスクリプトと同様に、相対的な順序を保持します。
たとえば、2つの遅延スクリプトlong.jsとsmall.jsがあるとします。
<script defer src="https://javascriptinfo.dokyumento.jp/article/script-async-defer/long.js"></script>
<script defer src="https://javascriptinfo.dokyumento.jp/article/script-async-defer/small.js"></script>
ブラウザは、パフォーマンスを向上させるために、ページのスクリプトをスキャンし、並列にダウンロードします。したがって、上記の例では、両方のスクリプトが並列にダウンロードされます。small.jsは最初に終了する可能性があります。
…しかし、defer属性は、ブラウザに「ブロックしない」ように指示するだけでなく、相対的な順序が維持されることを保証します。そのため、small.jsが先にロードされても、long.jsの実行後に待機して実行されます。
これは、JavaScriptライブラリを読み込んでから、それに依存するスクリプトを読み込む必要がある場合に重要になる可能性があります。
defer属性は、外部スクリプトに対してのみ使用できます。<script>タグにsrcがない場合、defer属性は無視されます。
async
async属性は、deferと somewhat 似ています。これもスクリプトを非ブロッキングにします。しかし、動作には重要な違いがあります。
async属性は、スクリプトが完全に独立していることを意味します。
- ブラウザは
asyncスクリプト(deferのように)でブロックしません。 - 他のスクリプトは
asyncスクリプトを待たず、asyncスクリプトも他のスクリプトを待ちません。 DOMContentLoadedとasyncスクリプトはお互いを待ちません。DOMContentLoadedは、asyncスクリプトの前(asyncスクリプトがページの完了後に読み込みが終了した場合)に発生する可能性があります。- …または、
asyncスクリプトの後(asyncスクリプトが短いかHTTPキャッシュにあった場合)に発生する可能性があります。
言い換えると、asyncスクリプトはバックグラウンドで読み込まれ、準備ができたら実行されます。DOMや他のスクリプトはそれらを待たず、それらは何にも待ちません。読み込まれたら実行される完全に独立したスクリプトです。非常にシンプルですね?
deferで行ったものと同様の例を示します。2つのスクリプトlong.jsとsmall.jsがありますが、今度はdeferの代わりにasyncを使用しています。
それらは互いを待ちません。最初にロードされたもの(おそらくsmall.js)が最初に実行されます。
<p>...content before scripts...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM ready!"));
</script>
<script async src="https://javascriptinfo.dokyumento.jp/article/script-async-defer/long.js"></script>
<script async src="https://javascriptinfo.dokyumento.jp/article/script-async-defer/small.js"></script>
<p>...content after scripts...</p>
- ページの内容はすぐに表示されます。
asyncはそれをブロックしません。 DOMContentLoadedは、asyncの前と後の両方で発生する可能性があり、ここでは保証されません。- より小さいスクリプト
small.jsは2番目ですが、おそらくlong.jsの前にロードされるため、small.jsが最初に実行されます。ただし、キャッシュされている場合、long.jsが先にロードされて最初に実行される可能性もあります。言い換えると、asyncスクリプトは「ロード優先」の順序で実行されます。
asyncスクリプトは、カウンターや広告など、独立したサードパーティのスクリプトをページに統合する場合に最適です。これは、それらが私たちのスクリプトに依存しておらず、私たちのスクリプトがそれらを待機する必要がないためです。
<!-- Google Analytics is usually added like this -->
<script async src="https://google-analytics.com/analytics.js"></script>
async属性は、外部スクリプトに対してのみ使用できます。deferと同様に、<script>タグにsrcがない場合、async属性は無視されます。
動的スクリプト
ページにスクリプトを追加するもう1つの重要な方法があります。
JavaScriptを使用して、スクリプトを作成し、ドキュメントに動的に追加できます。
let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)
スクリプトは、ドキュメント(*)に追加されるとすぐに読み込みを開始します。
動的スクリプトは、デフォルトで「async」として動作します。
つまり
- 何も待たず、何も待ちません。
- 最初にロードされたスクリプトが最初に実行されます(「ロード優先」順)。
script.async=falseを明示的に設定すると、この動作を変更できます。その場合、スクリプトはdeferと同様に、ドキュメントの順序で実行されます。
この例では、loadScript(src)関数はスクリプトを追加し、asyncをfalseに設定します。
そのため、long.jsは常に最初に実行されます(最初に追加されるため)。
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
script.async = false;
document.body.append(script);
}
// long.js runs first because of async=false
loadScript("/article/script-async-defer/long.js");
loadScript("/article/script-async-defer/small.js");
script.async=falseがない場合、スクリプトはデフォルトのロード優先順序(おそらくsmall.jsが最初)で実行されます。
再び、deferと同様に、ライブラリを読み込んでからそれに依存する別のスクリプトを読み込む場合は、順序が重要になります。
まとめ
asyncとdeferの両方に共通する点は、そのようなスクリプトのダウンロードがページのレンダリングをブロックしないことです。そのため、ユーザーはページの内容を読み、すぐにページに慣れることができます。
しかし、それらには重要な違いもあります。
| 順序 | DOMContentLoaded |
|
|---|---|---|
async |
ロード優先順序。ドキュメントの順序は関係ありません。最初にロードされたものが最初に実行されます。 | 無関係。ドキュメントがまだ完全にダウンロードされていない間にロードおよび実行される可能性があります。これは、スクリプトが小さく、またはキャッシュされており、ドキュメントが十分に長い場合に発生します。 |
defer |
ドキュメント順序(ドキュメントにあるとおり)。 | ドキュメントがロードおよび解析された後(必要に応じて待機)、DOMContentLoadedの直前に実行されます。 |
実際には、deferは、全体のDOMを必要とするスクリプト、またはそれらの相対的な実行順序が重要なスクリプトに使用されます。
そして、asyncは、カウンターや広告など、独立したスクリプトに使用されます。それらの相対的な実行順序は重要ではありません。
注意:deferまたはasyncを使用している場合、ユーザーはスクリプトがロードされる前にページが表示されます。
そのような場合、一部のグラフィックコンポーネントはまだ初期化されていない可能性があります。
「読み込み中」の表示を配置し、まだ機能していないボタンを無効にすることを忘れないでください。ユーザーがページで何ができるのか、何がまだ準備中なのかを明確に表示してください。
コメント
<code>タグ、複数行なら<pre>タグを使用。10行以上は、plnkr、jsbin、codepenなどのサンドボックスをご利用ください。