やあ、お疲れ様。
最近、チームのコードレビューをしていて「とりあえず `async` つけとけば速くなるっしょ」的なノリで `script` タグを書いてるコードを見かけてね、少しブラウザの裏側の動きを整理しておいた方がいいなと思ったんだ。
フロントエンドの中級から一段上のシニアへステップアップするにあたって、「ブラウザがHTMLをどう読み、どう解釈し、どこでJavaScriptをねじ込んでいるのか」というレンダリングパイプラインの全体像を正確に理解しておくことは、パフォーマンスチューニングの武器として絶対に欠かせない。
今回は、HTMLパースをブロックせずにJSを走らせるための魔術である `defer`、`async`、そして現代の標準である `type=”module”` の挙動について、ブラウザの裏側の事情を交えながら徹底的に紐解いていこうか。
—
1. 前提:なぜ素の `` は悪者扱いされるのか?
まず大前提として、ブラウザがHTMLファイルを受け取ってから画面を表示するまでの流れを思い出してほしい。
ブラウザのパーサー(HTML Parser)は、ネットワーク経由で送られてきたバイトストリームを文字に直し、トークンに分解し、DOM(Document Object Model)ツリーを上から順に組み立てていく。この真っ最中に、HTMLの中に普通の `` が出現したとする。
すると、ブラウザはこう判断する。 > 「おっと、このJavaScriptがDOMの構造をごっそり書き換えたり、`document.write` なんていう爆弾を投下するかもしれない。安全のために、HTMLのパースをいったん完全に中断しよう」
これが、パーサーブロッキング(Parser-blocking)と呼ばれる現象だ。 さらに厄介なことに、外部JSファイルの場合、ブラウザはまずネットワーク経由でそのスクリプトを「ダウンロード」し、その上で「実行」し終わるまで、次のHTMLパースに進めない。その間、メインスレッドは完全に占有され、画面は真っ白なままユーザーを待たせることになる。これがユーザー体験(UX)を致命的に悪化させる原因だ。
この「ネットワーク待ち」と「実行待ち」という2重の呪縛からHTMLパースを解放するために生まれたのが、`defer` と `async`、そして `type="module"` なんだ。
---
2. `defer` と `async` の決定的な違い
この2つの属性は、どちらも「バックグラウンドで非同期にスクリプトをダウンロードする」という点では同じだ。だが、「いつ実行するのか」というタイミングにおいて、エンジニアの意図を裏切るかどうかの決定的な違いがある。
`defer`(遅延実行:HTMLパース完了後に順序を保って実行)
- ダウンロード: HTMLパースと並行してバックグラウンドで行われる(パーサーをブロックしない)。
- 実行タイミング: HTMLのパースが完全に完了した後、かつ `DOMContentLoaded` イベントが発火する直前に実行される。
- 順序の保証: 複数の `defer` スクリプトがある場合、HTMLに書かれた上から順の実行順序が完全に保証される。
シニアの視点から言えば、「通常のアプリケーションコードのほとんどは、基本 `defer` を選んでおけば間違いない」。依存関係が崩れないし、DOMが確実にそこに存在(構築完了)している状態で安全にスクリプトを走らせることができるからだ。
`async`(非同期実行:ダウンロードが終わり次第、即座に実行)
- ダウンロード: HTMLパースと並行して行われる。
- 実行タイミング: ダウンロードが完了した瞬間、HTMLパースを強制的に一時中断して、その場で即座に実行される。
- 順序の保証: 一切なし。早くダウンロードが終わったスクリプトからランダムに実行される。
`async` は、他のスクリプトやDOMの構築に一切依存しない「完全な独立系スクリプト」のためにある。代表例は、Googleアナリティクスなどのアクセス解析タグや、広告タグだ。これらはページがどうなっていようと、勝手に読み込まれて勝手に動いてくれればいいので、`async` が最適解になる。
---
3. 現代のスタンダード:`type="module"` のデフォルト挙動
さて、モジュールバンドラ(ViteやWebpackなど)全盛の現代において、ES Modules(ESM)を使う機会が当たり前になった。ここで一つ、非常に重要な事実を覚えておいてほしい。
HTML内で `` を書いた場合、デフォルトでその挙動は自動的に `defer` になる。
明示的に `defer` と書かなくても、ブラウザはモジュールスクリプトをHTMLパースと並行してダウンロードし、DOMの構築がすべて終わってから(かつ依存関係のグラフを解決した上で)実行してくれる。
さらに、モジュールスクリプトに `async` を付与することも可能だ。
`` と書いた場合、こちらは「ダウンロードが終わり次第、依存関係を解決して即座に実行(HTMLパースは中断される)」という振る舞いに変わる。これもウィジェット系や独立したコンポーネントの読み込みで重宝するテクニックだ。
---
4. 現場で使える!実践的なコードパターン
では、実際のモダンなWebアプリケーション開発において、これらの属性をどう使い分けるべきか。実務でそのまま使える具体例を見ていこう。
パターンA:メインのアプリケーションエントリーポイント(`defer` 相当の挙動)
アプリのロジックやUIをコントロールするメインのJSは、DOMが確実に構築された後に動いてほしい。ES Modulesを使うのが現代のセオリーだ。
パターンB:依存関係のないサードパーティ製スクリプト(`async` の活用)
ページ内の他の要素に依存せず、かつ他のスクリプトを待たせる必要がないトラッキングコードや計測ツールの場合。
メインコンテンツ
パターンC:古いブラウザ(IEなど、もう絶滅危惧種だが…)へのフォールバック
もし、まだレガシーな環境をサポートする必要がある場合、`nomodule` 属性を組み合わせることで、モダンブラウザとレガシーブラウザで読み込むスクリプトを綺麗に切り分けることができる。
---
5. チーフアーキテクトからの実践的なアドバイス
最後に、実務でパフォーマンスチューニングを行う際に見落としがちな罠を一つ伝えておこう。
「じゃあ、全部のスクリプトに `async` や `defer` をつければ完璧だな!」と短絡的に考えてはいけない。
特に `async` を不適切なスクリプト(例えば、他の自作ライブラリ関数に依存しているスクリプトなど)に付けてしまうと、「たまに再現する、タイミング依存の謎の `TypeError: Cannot read properties of undefined`」という、デバッグ泣かせの亡霊を生み出すことになる。
スクリプトの依存関係を頭の中でしっかりマッピングし、
1. メインのアプリロジック: `type="module"`(実質 `defer`)で安全に、順序を守って動かす。
2. 独立した外部ツール(アナリティクスや広告): `async` で一刻も早く非同期で読み込ませる。
この基本原則をチーム全員が共通認識として持てば、Webページの初期表示速度(Core Web Vitalsの指標、特にINPやLCP)は劇的に改善されるはずだ。
明日からのコードレビューで、「お、このscriptタグ、ちゃんと特性を理解して選んでるな」と後輩のコードにニヤリとできることを期待しているよ。さて、次のタスクに取り掛かろうか。

コメント