JavaScript
performance
webperf

<script> タグに async / defer を付けた場合のタイミング

HTML5 においては、<script> タグに、deferasync 属性を付与することで、これらの読込が HTML パースを妨害しないようにすることができます。これにより、サイトのメインコンテンツの読込がある程度高速化されます。

async にせよ defer にせよ、それほど詳細に動作を解説した記事が見つかりませんでしたので、本記事では、これらの動作について解説します。

イベントについて

まずは、asyncdefer の挙動を理解するために重要な、2つのイベントについて理解しましょう。
両者とも、MDN に十分な情報があるため、本記事ではそれほど詳細には解説しません。

DOMContentLoaded

HTML のパースが完了した直後に発火します。

MDN

load

HTML のパースが完了し、CSS や画像などのダウンロードと表示、JavaScript ファイルのダウンロードと実行など、全てのリソースの処理が完了した (このタイミングを Document Complete とも呼びます) 直後に発火します。

MDN

読込の方法

どのようなタイミングで関して、WHATWG の規格書には、非常にわかりやすい図が添えられています。

これをベースに、イベント等を追加し、動作の図を作成しました。
<script> タグの読込タイミングに関する図

1. 同期的読込 (デフォルト)

<script> タグに deferasync も付けないと、HTML パース中に <script> タグにたどり着き次第、パースを一時停止し、JS ファイルのダウンロードと実行を行います。
JS ファイルの実行が完了するまで、HTML のパースは再開されません。

実装コード:

<script src="./script.js">

2. async

<script> タグに async 属性を追加することで、非同期に JS ファイルをダウンロード・実行します。

ダウンロードによって、HTML のパースを止めることはなく、並行してダウンロードが進みます。
ダウンロード完了後即 JS ファイルは実行され、実行中は HTML パースが一時停止します。
ダウンロードは HTML パースなどを行うメインスレッドとは、別のスレッドで実行される一方、JS ファイルの実行自体は、同じメインスレッドで行われるため、HTML パースと同時並行で実行されるわけではないことに注意して下さい。

また、複数の <script> タグが async 付きで記述されている場合、それらの実行順序は <script> タグを書いた順には実行されず、ランダムになります。

上の図では、ダウンロード・実行共にパース前に完了していますが、ダウンロード完了 (≒ 実行開始) や実行完了が、DOMContentLoaded イベント発火後になる場合もあります。
load イベントよりは前に実行されます。JavaScript ファイルの実行完了が、load イベント発火の条件でもあるからです。

実装コード:

<script src="./script.js" async>

3. defer

<script> タグに defer 属性を追加することで、HTML パース完了後、DOMContentLoaded イベントの直前に (※WHATWG 仕様) JS ファイルを実行します。
実行はパース完了後ですが、ダウンロードは async 同様 <script> タグがパースされ次第即座に非同期に (メインスレッドとは別のスレッドで) 実行されます。

また、async とは異なり、複数の <script> タグが defer 付きで記述されている場合、<script> タグを書いた順に実行されるのも特徴です。

例えば、

<script src="/a.js" defer></script>
<script src="/b.js" defer></script>
<script src="/c.js" defer></script>

というような記述の場合、a.js の処理が完了してから b.js が実行され、b.js の処理が実行されてから c.js の処理が実行されます。

実装コード:

<script src="./script.js" defer>

どれが速いのか

先程の図では、HTML パースの処理にかかる時間がバラバラになっていましたので、統一してみたグラフも作成してみました。
<script> タグの読込タイミングに関する図 ― 処理の時間を統一したもの
まず、async や defer に比べて、デフォルト (どちらの属性もつけない場合) は遅いです。これは、JS ダウンロードにより、HTML パースが停止されるためです。

async と defer を比較した場合、どちらが速いと言い切るのは難しいのですが、今のところ私は defer が良いのではないかと思っています。

前述のように、async の場合、ダウンロード完了直後に JS が実行されます。従って、async の場合 JS が実行されるタイミングは、load イベントの前ということ以外、いつ実行されるのかわかりません。

HTML パースの完了前にダウンロードが完了すれば、JS の実行で HTML パースがブロックされる可能性があります。この場合は HTML パース完了が遅くなります。
ダウンロード完了が HTML パース完了後の場合、当然 HTML パースはブロックされません。図では、JS の実行完了までの時間が defer よりも遅いですが、通常はレンダリングや、画像等他のリソース読込などの処理も行われているため、load までの時間を遅らせることはあまりないかと思います。

ダウンロード完了が HTML パース完了後の場合でも、defer に比べて大きなパフォーマンス上のメリットはないように思われるため、確実に実行を遅らせることができる defer の方が良いのではないか、というのが、今のところの私の結論です。