レンダリングブロックとパース阻害(defer/async/moduleの差)
なぜ head の script で表示が止まり、defer と async で結果が変わるのか。取得と実行のタイミング、パーサ阻害の有無、実行順序保証の差を内部から整理し、置き場所の判断を確実にします。
- 1.無印 script は取得も実行もその場でパースをブロックし、async は取得が並列でも実行時にパースを止める。defer と type=module(無印)はパースを一切止めず、完了後にまとめて実行する。
- 2.実行順序の保証は無印と defer が記述順、async と type=module async は取得完了順で順不同。defer だけがパース非阻害と記述順保証を両立する。
- 3.type=module は既定で defer 相当の遅延・記述順を持ち、依存グラフ全体を取得してから実行する。async を併記すると遅延と順序保証の両方が外れる。
なぜ script の指定で表示が止まったり止まらなかったりするのか
ブラウザのHTMLパーサは、トークンを先頭から順に消費してDOMを直列に組み立てます。問題は <script> に出会ったときの振る舞いで、ここに取得(フェッチ)のタイミングと実行のタイミングという2つの軸があり、async / defer / type=module はこの2軸をそれぞれ別の組み合わせに切り替える属性です。「パースを止めるか」は実行軸の問題で、取得が並列でも実行でパースが止まれば描画は遅れます。基礎の全体像は クリティカルレンダリングパスの最適化原理 を、取得前倒しの仕組みは ブラウザの投機的パース処理とリソース優先度 を参照してください。
なぜ無印 script がパースを止めるのか。理由はスクリプトが document.write で後続のトークン列を書き換えうるためです。書き換えの可能性がある以上、ブラウザはスクリプト実行を終えるまで先のDOMを確定できません。逆に言えば、document.write を使わないと約束できる属性(async / defer / module)ではこの直列化を外せます。
4つの読み込みモードの取得・実行・阻害
外部 script(src あり)の挙動は、属性の組み合わせで次の4モードに整理できます。インライン script(src なし)は別扱いで、async / defer は無効、type=module のみ遅延します。
| 指定 | 取得タイミング | 実行タイミング | パース阻害 | 実行順序 |
|---|---|---|---|---|
| script(無印) | 発見後すぐ・並列 | 取得完了後すぐ・その場 | する(取得中も実行中も) | 記述順 |
| script defer | 発見後すぐ・並列 | パース完了後・DOMContentLoaded前 | しない | 記述順 |
| script async | 発見後すぐ・並列 | 取得完了次第・割り込み実行 | 実行の瞬間だけ止める | 取得完了順(順不同) |
| script type=module | 発見後・依存も含め並列 | パース完了後(defer相当) | しない | 記述順 |
ここで誤解しやすいのが無印 script の取得です。現代のブラウザはプリロードスキャナで src を先読みし、取得自体は並列に始めます。それでも無印が「ブロックする」と言われるのは、取得が終わるとその場で即実行し、実行が済むまでパースを再開しないからです。取得の並列化とパース阻害は別レイヤーの話です。
async は取得を並列で進めますが、取得が完了した時点でパースに割り込んで即実行します。完了タイミングはネットワーク次第なので、実行が走る位置はパースのどこになるか予測できません。結果として複数の async script は取得が速い順に実行され、記述順は保証されません。互いに依存する分割スクリプトに async を使うと、依存先が後に実行されて壊れる典型的な事故になります。
defer と async の本質的な違い:順序保証と実行点
async と defer はどちらも「パースを止めない」点で同じに見えますが、実行点と順序保証が正反対です。
- defer:取得は並列で先に進めつつ、実行はHTMLパースが完全に終わるまで待つ。複数の defer script は記述順に、
DOMContentLoadedの発火直前にまとめて実行されます。パース非阻害と記述順の両立はこのモードだけの性質です。 - async:取得が終わり次第その場で実行するため、パース途中に割り込みます。順序は取得完了順で順不同、
DOMContentLoadedを待たず、場合によってはそれより前にも後にも走ります。
<!-- 並列取得・記述順実行・パース後にまとめて実行。アプリの本体に最適 -->
<script src="framework.js" defer></script>
<script src="app.js" defer></script> <!-- 必ず framework.js の後に実行される -->
<!-- 並列取得・取得完了順で即実行・他に依存しない計測系に最適 -->
<script src="analytics.js" async></script>
この差から実務の使い分けが決まります。互いに依存する、またはDOM構築の完了を前提とするスクリプトは defer。他のどのスクリプトともDOMとも独立した計測・広告タグは async。DOMContentLoaded を含む発火順序の全体像は イベントループの内部構造 と合わせて押さえると、初期化処理を置く位置が定まります。
type=module の振る舞い:既定が defer 相当である理由
<script type=module> は、属性を何も足さなくても既定で defer 相当です。すなわちパースを止めず、HTMLパース完了後・DOMContentLoaded 前に記述順で実行されます。インライン module(src なし)も同じく遅延する点が、インライン無印 script との大きな違いです。
なぜ defer 相当なのか。module は import で他のモジュールへ依存しうるため、ブラウザは実行前に依存グラフ全体を取得・解析する必要があります。エントリを取得し、import 文を静的解析して依存先を芋づる式に取得し、グラフが揃ってから評価する、という多段の手順を踏むため、そもそも「その場で即実行」が成り立ちません。さらに module には次の固有性質があります。
| 観点 | クラシック script(無印/defer/async) | type=module |
|---|---|---|
| 既定の遅延 | 無印は遅延なし | 常に defer 相当で遅延 |
| 実行回数 | 同じURLでも記述ごとに実行 | URLごとに一度だけ評価(重複排除) |
| スコープ | グローバル共有 | モジュールスコープで隔離 |
| strict mode | 明示が必要 | 常に strict |
| 依存解決 | なし(手動で順序管理) | import で静的に解決 |
<script type=module async> とすると、module の既定だった「遅延・記述順」が外れ、依存グラフが揃い次第その場で割り込み実行するようになります。クラシックの async と同様に順序は順不同です。依存グラフの取得を待つ点は変わりませんが、パース完了を待たずに走るため、独立した module を最速で動かしたいときだけ使います。
nomodule と後方互換、そして優先度
type=module を解さない古いブラウザに別ファイルを当てるための属性が nomodule です。module 対応ブラウザは nomodule 付き script を無視し、非対応ブラウザは type=module を不明な type として無視します。この排他で、新旧へ別バンドルを出し分けられます。
<!-- 新しいブラウザはこちらを実行(type=module を理解する) -->
<script type="module" src="app.modern.js"></script>
<!-- 古いブラウザはこちらを実行(nomodule を無視できない=実行する) -->
<script nomodule src="app.legacy.js" defer></script>
リソース取得の内部優先度も属性で変わります。無印で早い位置の script は High、async / defer の script は描画を妨げないため Low に置かれるのが一般的です(Chromium 基準の概略)。つまり defer は「パースを止めず、かつ帯域を重要リソースに譲る」方向にも働きます。取得前倒しと優先度の関係は ブラウザのレンダリングの仕組み も参照してください。
実務での判断指針
問われるのは、(1) 無印は取得が並列でも実行でパースを止める点、(2) async はパース非阻害に見えて「実行の瞬間」は割り込んで止め、順序が取得完了順で順不同な点、(3) defer だけがパース非阻害と記述順保証を両立し DOMContentLoaded 前にまとめて実行される点、(4) type=module は既定で defer 相当・URLごと一度・常時 strict で、async 併記で遅延と順序が外れる点、の4つです。
- アプリ本体は defer か module:DOM完成を前提とし、相互依存があるコードは記述順が保証される defer、またはモジュールグラフを解決する module へ。
- 独立タグは async:他に依存しない計測・広告は async で最速取得・即実行に。
- head の無印 script を避ける:どうしても無印を使うなら body 末尾へ。head に置くと取得後の即実行でパースが直列に止まる。
- module の async は限定的に:既定の遅延・記述順を捨ててよい独立 module のみ。
まとめ
4つのモードは取得と実行の2軸で整理できます。無印は取得が並列でも実行でその場のパースを止め、記述順に実行。async は取得を並列化するが実行の瞬間だけ割り込んで止め、順序は取得完了順で順不同。defer は取得を並列化し実行をパース完了後まで遅らせ、記述順を保ち DOMContentLoaded 前にまとめて実行する唯一の「非阻害かつ順序保証」モード。type=module は既定で defer 相当に加え、依存グラフ解決・URLごと一度・常時 strict を備え、async 併記で遅延と順序保証が外れます。置き場所と属性は「相互依存があるか」「DOM完成を前提とするか」「他から独立か」で決めれば外しません。前後の文脈は クリティカルレンダリングパスの最適化原理 と ブラウザの投機的パース処理とリソース優先度 で補完できます。
Web/フロントエンド Article
レンダリングブロックとパース阻害(defer/async/moduleの差)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ブラウザ
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 6
導入後に効く点
実行順序の保証は無印と defer が記述順、async と type=module async は取得完了順で順不同。defer だけがパース非阻害と記述順保証を両立する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 6
判断チェックリスト
- 自社の用途が「ブラウザ / HTML」に近いか確認する。
- 強みである「無印 script は取得も実行もその場でパースをブロックし、async は取得が並列でも実行時にパースを止める。defer と type=module(無印)はパースを一切止めず、完了後にまとめて実行する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。