【JavaScript】 ページ読み込み時の実行タイミングを検証してみる
更新日:2020/06/30
JavaScriptはブラウザ表示をブロックするので、</body>の直前に記述するべきという話を聞いた。
本当だろうか?
少し検証してみることにした。
OS:
Windows 10
検証ブラウザ:
FireFox 72.0.2
Google Chrome 79.0.3945.130
Microsoft Edge 44.18362.449.0
スクリプトの実行タイミング
ネットで調べてみると、ページ読み込み時のJavaScriptの実行タイミングは次のようになっているようだ。
(1) 記述した順番に実行される
(2) body内のスクリプトは、ブラウザ表示をブロックする。
この二つについて、検証するためにコードを作成してみます。
js-timing.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- **↓↓↓共通関数の定義**** -->
<script>
// 日時取得
const tm =()=>{
let d = new Date();
return d.getHours().toString() + ":"
+ d.getMinutes().toString() + ":"
+ d.getSeconds().toString() + ":"
+ d.getMilliseconds().toString();
};
// コンソール出力
const mylog=(msg,color)=>console.log( "%c" + tm() + " " + msg + " %c// "
+ ( ((new Date()).getTime() - sttime.getTime()) / 1000).toString() + " 秒経過" , color , "color:#ccc" );
// 5秒間待つ
const jswait = ( msg , color ) =>{
let st = new Date();
let et;
do{
et = new Date();
}while( (et.getTime() - st.getTime() ) < 5000 );
mylog(msg,color);
};
const sttime = new Date();
mylog( "**********start" , "color:black");
</script>
<!-- **↓↓↓【js1】js-timing1.js の読み込み**** -->
<script src="./js-timing1.js" type="text/javascript"></script>
<!-- **↓↓↓【head】内で実行されるコード**** -->
<script>
mylog( "head" , "color:red" );
jswait( "head loop end" , "color:red");
window.addEventListener( "load" , function() {
mylog( "load(head)" , "color:red");
});
window.addEventListener( "DOMContentLoaded" , function() {
mylog( "DOMContentLoaded(head)" , "color:red");
});
</script>
</head>
<body>
<p>line1</p>
<!-- **↓↓↓【js2】js-timing2.js の読み込み**** -->
<script src="./js-timing2.js" type="text/javascript"></script>
<p>line2</p>
<!-- **↓↓↓【body】内で実行されるコード**** -->
<script>
mylog( "body" , "color:blue" );
jswait( "body loop end" , "color:blue" );
window.addEventListener( "load" , function() {
mylog( "load(body)" , "color:blue" );
});
window.addEventListener( "DOMContentLoaded" , function() {
mylog( "DOMContentLoaded(body)" , "color:blue" );
});
</script>
<p>line3</p>
<!-- **↓↓↓【js3】js-timing3.js の読み込み**** -->
<script src="./js-timing3.js" type="text/javascript"></script>
<p>line4</p>
</body>
</html>
上のhtmlで読み込んでいる3つのJavaScriptファイルのコードは、次の通り。
【js1】 js-timing1.js
(function ( msg , color ) {
mylog(msg,color);
jswait(msg + " loop end",color);
window.addEventListener( "load" , function() {
mylog( "load(" + msg + ")" , color );
});
window.addEventListener( "DOMContentLoaded" , function() {
mylog( "DOMContentLoaded(" + msg + ")" , color);
});
})( "js1" , "color:brown" );
【js2】 js-timing2.js
(function ( msg , color ) {
mylog(msg,color);
jswait(msg + " loop end",color);
window.addEventListener( "load" , function() {
mylog( "load(" + msg + ")" , color );
});
window.addEventListener( "DOMContentLoaded" , function() {
mylog( "DOMContentLoaded(" + msg + ")" , color);
});
})( "js2" , "color:black" );
【js3】 js-timing3.js
(function ( msg , color ) {
mylog(msg,color);
jswait(msg + " loop end",color);
window.addEventListener( "load" , function() {
mylog( "load(" + msg + ")" , color );
});
window.addEventListener( "DOMContentLoaded" , function() {
mylog( "DOMContentLoaded(" + msg + ")" , color);
});
})( "js3" , "color:green" );
各スクリプトの処理:
(1) コンソールに開始ログ出力
(2) 5秒待つ(処理をブロック)
(3) loadイベント登録
(4) DOMContentLoadedイベント登録
html上のスクリプト二つと、外部から読み込んでいる三つのスクリプトは、ほぼ同じ処理をおこなっています。
これらのスクリプトから呼び出される関数の一部は、html上に共通関数として抜き出しています。
実行結果
デベロッパツール:コンソール
16:18:6:664 **********start // 0 秒経過
16:18:6:690 js1 // 0.026 秒経過
16:18:11:690 js1 loop end // 5.026 秒経過
16:18:11:691 head // 5.027 秒経過
16:18:16:692 head loop end // 10.028 秒経過
16:18:16:698 js2 // 10.034 秒経過
16:18:21:699 js2 loop end // 15.035 秒経過
16:18:21:703 body // 15.039 秒経
16:18:26:704 body loop end // 20.04 秒経過
16:18:26:715 js3 // 20.051 秒経過
16:18:31:715 js3 loop end // 25.051 秒経過
16:18:31:769 DOMContentLoaded(js1) // 25.105 秒経過
16:18:31:769 DOMContentLoaded(head) // 25.105 秒経過
116:18:31:769 DOMContentLoaded(js2) // 25.105 秒経過
16:18:31:769 DOMContentLoaded(body) //25.105 秒経過
16:18:31:770 DOMContentLoaded(js3) // 25.106 秒経過
16:18:31:773 load(js1) // 25.109 秒経過
16:18:31:773 load(head) // 25.109 秒経過
1616:18:31:773 load(js2) // 25.109 秒経過
16:18:31:775 load(body) // 25.111 秒経過
16:18:31:775 load(js3) // 25.111 秒経過
上の実行結果を見ると、head内やbody内など関係なく、htmlに現れる順番でJavaScriptを実行しているのがわかります。
その際、外部ファイルについては、読み込みの完了を待っています。
そして全てのJavaScriptの処理を終えてから、DOMContentLoadedイベントが発生しています。
DOMContentLoadedは、DOMの構築が終わった時点で発生するイベントです。
ブラウザにページが表示されるのは、このイベント以降になります。
どうやら、スクリプトがブラウザ表示を妨げるのは本当のようですね。
defer または async を付けた場合のタイミング
外部のJavaScriptファイル読み込み時に「defer」または「async」を付加すると非同期で実行されます。
ブラウザのレンダリングを妨げないので、bodyの直前に記述しなくてもよさそうな気がします。
こちらについても確認してみます。
async / defer 指定例
<script async src="./js-timing1.js" type="text/javascript"></script>
<script defer src="./js-timing1.js" type="text/javascript"></script>
「defer」「async」は、次のようなインラインスクリプトに指定することができません。
<script defer> ← 指定できない
・・・コード
</script>
実際にどのような動きになるのか、確認してみます。
3つの外部JavaScriptファイルに「defer」を付加
deferを付加されたスクリプトは、htmlが解析された後に実行されます。
ただし全てのdeferスクリプト終了までDOMContentLoadedイベントが発生しません。
また処理は、記述した順番でおこなわれます。
最初に挙げたhtmlの外部スクリプト読み込みに対して、「defer」して実行してみます。
defer 結果 デベロッパツール:コンソール
18:6:58:463 **********start // 0.001 秒経過
18:6:58:464 head // 0.001 秒経過 インラインスクリプト
18:7:3:465 head loop end // 5.002 秒経過
18:7:3:465 body // 5.002 秒経過インラインスクリプト
18:7:8:465 body loop end // 10.002 秒経過
18:7:8:484 js1 // 10.021 秒経過外部スクリプト
18:7:13:484 js1 loop end // 15.021 秒経過
18:7:13:485 js2 // 15.022 秒経過外部スクリプト
18:7:18:485 js2 loop end // 20.022 秒経過
18:7:18:486 js3 // 20.023 秒経過外部スクリプト
18:7:23:486 js3 loop end // 25.023 秒経過
18:7:23:546 DOMContentLoaded(head) // 25.083 秒経過
18:7:23:546 DOMContentLoaded(body) // 25.083 秒経過
18:7:23:546 DOMContentLoaded(js1) // 25.083 秒経過
18:7:23:546 DOMContentLoaded(js2) // 25.083 秒経過
18:7:23:546 DOMContentLoaded(js3) // 25.083 秒経過
18:7:23:547 load(head) // 25.084 秒経過
18:7:23:547 load(body) // 25.084 秒経過
18:7:23:547 load(js1) // 25.084 秒経過
18:7:23:547 load(js2) // 25.084 秒経過
18:7:23:547 load(js3) // 25.084 秒経過
上の結果を見ると、インラインスクリプトの処理が終わった後に、外部スクリプトが処理されています。
そして全てのスクリプトが処理されてから、DOMContentLoadedイベントが発生しています。
3つの外部JavaScriptファイルに「async」を付加
asyncを付加されたスクリプトは非同期で読み込まれ、読み込み完了次第実行されます。
これはhtml上での記述順に左右されないということを意味します。
DOMContentLoadedイベントの扱いについては、不明です。
async結果 デベロッパツール:コンソール
18:14:56:614 **********start // 0 秒経過
18:14:56:616 head // 0.002 秒経過
18:15:1:616 head loop end // 5.002 秒経過
18:15:1:618 body // 5.004 秒経過
18:15:6:619 body loop end // 10.005 秒経過
18:15:6:650 DOMContentLoaded(head) // 10.036 秒経過
18:15:6:650 DOMContentLoaded(body) // 10.036 秒経過
18:15:6:654 js1 // 10.04 秒経過
18:15:11:655 js1 loop end // 15.041 秒経過
18:15:11:656 js2 // 15.042 秒経過
18:15:16:657 js2 loop end // 20.043 秒経過
18:15:16:659 js3 // 20.045 秒経過
18:15:21:659 js3 loop end // 25.045 秒経過
18:15:21:660 load(head) // 25.046 秒経過
18:15:21:660 load(body) // 25.046 秒経過
18:15:21:661 load(js1) // 25.047 秒経過
18:15:21:661 load(js2) // 25.047 秒経過
18:15:21:662 load(js3) // 25.048 秒経過
この結果を見た限りでは、DOMContentLoadedイベント発生後にasyncスクリプトが実行されています。
スクリプト内でDOMContentLoadedイベントを補足したい場合、「defer」を使用したほうがよさそうだ。
補足記事:【JavaScript】 script asyncはDOMContentLoadedを捕捉できない
「defer」の方が使い勝手がよい
「async」は実行順が不定なため、依存関係があるスクリプトファイルには使用できない。
またhtml解析直後のイベント(DOMContentLoaded)を補足できない。
基本的には、単体で動作かつDOM要素の操作をおこなわない、Webページとしてはなくても表面上特に問題ないスクリプトに使用した方がよさそう。
実際にはそれ以外の場面が多そうなので、「defer」の方が使い勝手がよい。
まとめ
JavaScriptはブラウザ表示をブロックします。
</body>の直前に配置したり、deferまたはasyncを使うようにしましょう。
更新日:2020/06/30
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。