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

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

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

イベントについて
==============

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

### `DOMContentLoaded`

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

[MDN](https://developer.mozilla.org/ja/docs/Web/Reference/Events/DOMContentLoaded)

### `load`

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

[MDN](https://developer.mozilla.org/ja/docs/Web/Reference/Events/load)

読込の方法
===========

どのようなタイミングで読み込まれるかに関して、[WHATWG の規格書](https://html.spec.whatwg.org/multipage/scripting.html#attr-script-async)には、非常にわかりやすい図が添えられています。

これをベースに、イベント等を追加し、動作の図を作成しました。
![<script> タグの読込タイミングに関する図](https://qiita-image-store.s3.amazonaws.com/0/41297/94f6efd2-1fbd-3d2c-8f80-ad17aaac4df6.png)


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

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

実装コード:

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

### 2. `async`

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

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

また、複数の `<script>` タグが `async` 付きで記述されている場合、それらの実行順序は `<script>` タグを書いた順には実行されないことにも注意が必要です。

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

実装コード:

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

### 3. `defer`

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

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

例えば、

```html
<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 の処理が実行されます。

実装コード:

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

どれが速いのか
==============

先程の図では、HTML パースの処理にかかる時間がバラバラになっていましたので、統一してみたグラフも作成してみました。
![<script> タグの読込タイミングに関する図 ― 処理の時間を統一したもの](https://qiita-image-store.s3.amazonaws.com/0/41297/efc08621-a5ec-d36b-59a7-3f9a85d54f5a.png)
まず、async や defer に比べて、デフォルト (どちらの属性もつけない場合) は遅いです。これは、JS ダウンロードにより、HTML パースが停止されるためです。

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

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

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

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