script・defer・asyncの実行順序
先に書いたa.jsの応答を止め、後のb.jsを先に取得できる同じHTMLを三方式で読みます。各scriptは実行時に本文のtarget要素が存在するか記録します。releaseは止めているscriptの応答を返すための演習用操作です。
説明
HTML内のscriptは、取得が終わった順にいつも実行されるわけではありません。通常の外部classic script、defer、asyncでは、HTML解析を待たせる条件と実行順の保証が異なります。DOMがまだ作られていない段階で参照すると、必要な要素が見つからないこともあります。
先に書いたa.jsの応答を止め、後のb.jsを先に取得できる同じHTMLを三方式で読みます。各scriptは実行時に本文のtarget要素が存在するか記録します。releaseは止めているscriptの応答を返すための演習用操作です。
1通常scriptで読込みを開始する
headにa、bの順で通常scriptを置いた文書を開きます。
(globalThis.normal=openDocument("/scripts?mode=sync")通常scriptの文書を開始,await waitForGate("sync-a")aの応答待ちを確認,{events:normal.frame.contentWindow.events,targetExists:本文が解析済みかBoolean(normal.frame.contentDocument.getElementById("target"))})aの取得待ちで本文のtargetはまだありません。通常のparser-inserted classic scriptは、実行が終わるまでその位置でHTML解析を待たせます。ブラウザの先読み取得と、DOMを作る解析の進行は別です。
2aを返して通常scriptを実行する
aの応答を許可し、文書のloadまで待ちます。
(await release("sync-a")aの応答を返す,await normal.loaded全資源の読込みを待つ,normal.frame.contentWindow.events実行順とDOM参照の結果)a、b、本文末尾のparserの順です。aとbが実行された時点ではtargetはなく、本文の解析後に記録するparserだけがtrueになります。scriptをHTML内のどこへ置くかがDOM参照の成否に関わります。
3読込み完了後のDOM
通常scriptの文書で、load後のtargetを確認します。
({state:normal.frame.contentDocument.readyState文書の読込み状態,target:normal.frame.contentDocument.getElementById("target")解析後の要素.textContent})load後にはtargetを取得できます。最初に見つからなかったのはidが間違っていたためではなく、実行時点でその要素がまだ作られていなかったためです。
4deferで同じHTMLを開始する
二本のscriptへdeferを付け、aの応答を待たせます。
(globalThis.deferred=openDocument("/scripts?mode=defer")defer付きの文書,await waitForGate("defer-a"),await waitForEventName(deferred.frame,"parser")本文末尾の解析を待つ,{events:deferred.frame.contentWindow.events,targetExists:Boolean(deferred.frame.contentDocument.getElementById("target"))})parserが記録され、targetもあります。defer付き外部classic scriptはHTML解析後に実行されるため、取得中も解析は先へ進めます。
5deferは記述順を保つ
aの応答を返し、全実行を待ちます。
(await release("defer-a")aの取得を完了させる,await deferred.loaded,deferred.frame.contentWindow.events解析後のa→bを確認)parserの後にa、bの順で実行され、両方ともtargetを参照できます。bを先に取得できても、defer付き外部classic scriptの実行順は文書内の順を保ちます。
6deferの取得完了順
資源記録からaとbの取得完了時刻を比較します。
(globalThis.deferResources=deferred.frame.contentWindow.performance.getEntriesByType("resource")scriptの取得記録.filter(e=>new URL(e.name).pathname==="/script.js"),{bFinishedBeforeA:取得完了順を比較deferResources.find(e=>new URL(e.name).searchParams.get("name")==="b").responseEnd<deferResources.find(e=>new URL(e.name).searchParams.get("name")==="a").responseEnd,execution:実行順を取得deferred.frame.contentWindow.events.map(e=>e.name)})bの取得が先に完了しても、実行されるのはaが先です。取得の完了順とJavaScriptの実行順は異なります。
7asyncで同じHTMLを開始する
二本にasyncを付け、aを待たせたままbの実行を待ちます。
(globalThis.asynchronous=openDocument("/scripts?mode=async")async付きの文書,await waitForGate("async-a"),await waitForEventName(asynchronous.frame,"b")aを待たずにbが実行されるか確認,asynchronous.frame.contentWindow.events)aが未完了でもbが実行されています。asyncは利用可能になったscriptから実行するため、記述順を保証しません。本文の存在も取得の速さと解析の進行に依存し、この例でtrueだったとしても一般に保証される条件ではありません。
8遅れたaが後から実行される
aを返してloadを待ちます。
(await release("async-a")遅れたaを返す,await asynchronous.loaded,asynchronous.frame.contentWindow.eventsbの後にaが実行された記録)bより後にaが実行されます。aが用意する変数をbが必要とするような依存関係がある場合、asyncの記述順だけに頼ることはできません。依存を明示するmoduleや、順序を保つ読込み方法を選びます。
9三方式の結果を並べる
取得条件が同じ三文書のscript実行順だけを抜き出します。
Object.fromEntries方式ごとに結果をまとめる([["normal",normal],["defer",deferred],["async",asynchronous]].map(([name,run])=>[name,run.frame.contentWindow.events.filter(e=>e.name!=="parser")scriptの実行だけを抽出.map(e=>e.name)]))通常とdeferはa→b、asyncはb→aです。通常はその位置で解析を待たせ、deferは解析後に順序を保ち、asyncは準備できたものを実行します。外部classic scriptについての比較で、module scriptには依存グラフなど別の規則があります。
まとめ
HTML解析の進行、資源の取得完了、script実行は別々の順序として確認します。
defer付き外部classic scriptは解析後に記述順で実行されます。
asyncは記述順とDOM完成を保証せず、script同士の依存を別に扱う必要があります。
操作履歴と解説を振り返れます。
ブラウザ / 描画・実行記録
(globalThis.normal=openDocument("/scripts?mode=sync"),await waitForGate("sync-a"),{events:normal.frame.contentWindow.events,targetExists:Boolean(normal.frame.contentDocument.getElementById("target"))}){
"events": [],
"targetExists": false
}(await release("sync-a"),await normal.loaded,normal.frame.contentWindow.events)[
{
"name": "a",
"hasTarget": false
},
{
"name": "b",
"hasTarget": false
},
{
"name": "parser",
"hasTarget": true
}
]({state:normal.frame.contentDocument.readyState,target:normal.frame.contentDocument.getElementById("target").textContent}){
"state": "complete",
"target": "本文"
}(globalThis.deferred=openDocument("/scripts?mode=defer"),await waitForGate("defer-a"),await waitForEventName(deferred.frame,"parser"),{events:deferred.frame.contentWindow.events,targetExists:Boolean(deferred.frame.contentDocument.getElementById("target"))}){
"events": [
{
"name": "parser",
"hasTarget": true
}
],
"targetExists": true
}(await release("defer-a"),await deferred.loaded,deferred.frame.contentWindow.events)[
{
"name": "parser",
"hasTarget": true
},
{
"name": "a",
"hasTarget": true
},
{
"name": "b",
"hasTarget": true
}
](globalThis.deferResources=deferred.frame.contentWindow.performance.getEntriesByType("resource").filter(e=>new URL(e.name).pathname==="/script.js"),{bFinishedBeforeA:deferResources.find(e=>new URL(e.name).searchParams.get("name")==="b").responseEnd<deferResources.find(e=>new URL(e.name).searchParams.get("name")==="a").responseEnd,execution:deferred.frame.contentWindow.events.map(e=>e.name)}){
"bFinishedBeforeA": true,
"execution": [
"parser",
"a",
"b"
]
}(globalThis.asynchronous=openDocument("/scripts?mode=async"),await waitForGate("async-a"),await waitForEventName(asynchronous.frame,"b"),asynchronous.frame.contentWindow.events)[
{
"name": "parser",
"hasTarget": true
},
{
"name": "b",
"hasTarget": true
}
](await release("async-a"),await asynchronous.loaded,asynchronous.frame.contentWindow.events)[
{
"name": "parser",
"hasTarget": true
},
{
"name": "b",
"hasTarget": true
},
{
"name": "a",
"hasTarget": true
}
]Object.fromEntries([["normal",normal],["defer",deferred],["async",asynchronous]].map(([name,run])=>[name,run.frame.contentWindow.events.filter(e=>e.name!=="parser").map(e=>e.name)])){
"normal": [
"a",
"b"
],
"defer": [
"a",
"b"
],
"async": [
"b",
"a"
]
}演習完了