一覧WB-13-2 · 描画とページの性能

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同士の依存を別に扱う必要があります。

操作履歴と解説を振り返れます。

ブラウザ / 描画・実行記録

1
(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
}
2
(await release("sync-a"),await normal.loaded,normal.frame.contentWindow.events)
[
  {
    "name": "a",
    "hasTarget": false
  },
  {
    "name": "b",
    "hasTarget": false
  },
  {
    "name": "parser",
    "hasTarget": true
  }
]
3
({state:normal.frame.contentDocument.readyState,target:normal.frame.contentDocument.getElementById("target").textContent})
{
  "state": "complete",
  "target": "本文"
}
4
(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
}
5
(await release("defer-a"),await deferred.loaded,deferred.frame.contentWindow.events)
[
  {
    "name": "parser",
    "hasTarget": true
  },
  {
    "name": "a",
    "hasTarget": true
  },
  {
    "name": "b",
    "hasTarget": true
  }
]
6
(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"
  ]
}
7
(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
  }
]
8
(await release("async-a"),await asynchronous.loaded,asynchronous.frame.contentWindow.events)
[
  {
    "name": "parser",
    "hasTarget": true
  },
  {
    "name": "b",
    "hasTarget": true
  },
  {
    "name": "a",
    "hasTarget": true
  }
]
9
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"
  ]
}

演習完了