TL

レンダリングブロックとパース阻害(defer/async/moduleの差)

なぜ head の script で表示が止まり、defer と async で結果が変わるのか。取得と実行のタイミング、パーサ阻害の有無、実行順序保証の差を内部から整理し、置き場所の判断を確実にします。

応用ブラウザHTMLJavaScriptパフォーマンスリソース読み込みES Modules最終更新: 2026-06-21
TL;DR要点だけ先に
  • 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 は取得を並列で進めますが、取得が完了した時点でパースに割り込んで即実行します。完了タイミングはネットワーク次第なので、実行が走る位置はパースのどこになるか予測できません。結果として複数の async script は取得が速い順に実行され、記述順は保証されません。互いに依存する分割スクリプトに async を使うと、依存先が後に実行されて壊れる典型的な事故になります。

defer と async の本質的な違い:順序保証と実行点

asyncdefer はどちらも「パースを止めない」点で同じに見えますが、実行点と順序保証が正反対です。

  • 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とも独立した計測・広告タグは asyncDOMContentLoaded を含む発火順序の全体像は イベントループの内部構造 と合わせて押さえると、初期化処理を置く位置が定まります。

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 で静的に解決
module に async を付けると挙動が変わる

<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、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

ブラウザHTMLJavaScriptパフォーマンスリソース読み込みブラウザHTMLJavaScript
参考: 公式情報