背景
<script>
, <link>
など
WEB開発・制作プロジェクトでよく見る要素でしたが、
その中、いろいろのコードを見ていたら
scriptの場所があるところはheadで定義されて、あるところはそうでなかったりすることがありました。
このように、<script>
, <link>
などのコードをその分見てきて「何か慣れているな」という気分もありつつ、
今のきっかけで「よりはっきり確かめたい」と思い、本記事を書くようになりました。
解説
headタグ
HTMLドキュメントにおいて、metadataを指定する要素です。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test WebApp</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="hello">
<h1 class="test-font">Hello World!</h1>
</div>
<script src="app.js"></script>
</body>
</html>
このように、stylesheet、link、meta、エンコーディング、SEO関連のデータなどのmetadataが格納されます。
これに伴い、
<link rel="stylesheet" href="style.css" />
の形でインポートされるCSSの場合も、スタイルシート・metadataになり、head内部に入るようになります。
bodyタグ
HTMLドキュメントの中身が定義されます。
基本的なDOM Node、Elementsが含まれており、
scriptコードにHTMLドキュメントのDOM Nodeなどを操作するロジックが含まれていれば
HTMLドキュメントが完全にParsing・ロードされてから実行されるのが大事です。
使い方
link
linkの場合はHTMLのmetadataを定義するため、head内部に入ります。
bodyの中にスタイルを指定しても適用はされますが、HTML5の基本的なSyntaxを守らないことになり、今後のメンテナンスが難しくなってコードの一貫性もよくなくなります。
script
scriptの場合は、普通に
の一番下に位置することが一般的でした。普通のJSでDOM要素を操作するときには、HTMLドキュメントを全て読み込んだ状態で回すことを前提にしているからです。
使い方に関しては二つがありますが、
<!-- Inline Script -->
<script>
const h2 = document.querySelector("h2.foo")
h2.addEventListener("mouseenter", () => { h2.innterText = "Mouse Enter" })
</script>
<!-- JS Import -->
<script src="app.js"></script>
普通にはインポートをした方がよく使われ、
たまにWordPressなどにinline scriptを書いたりすることがありました。
って、scriptはどっちやねん?
のために、scriptがfetch・実行されるまでのプロセスを確かめる必要があります。
body
の最下
HTMLドキュメントのParsingが終わってからScriptをダウンロード→実行します。
伝統的に使われた実装。
<head>
内部(async, deferなし)
async・deferのBoolean Attributesを定義せずscriptを宣言すると、このような動きになります。
一般的にも、「scriptをhead内部にすることはいいルールではない」などの話を聞かれたり、似ている記事があったかもしれません。
- HTMLドキュメントがParsingされる前に、scriptをダウンロード・実行するため、scriptが大きくなると「ローディングがちょっと重くない?」などのようにパフォーマンス面での副作用がありえる
- querySelector、getElementByIdなどのDOM要素を読み込んで指定するコートがあれば、script実行時点でDOMが見られていないため、動作が失敗する恐れあり
- document.addEventListener("load", callback)のように実装したら回避はできるが、効率的ではない
DOM要素を操作するコードがあるなら、このようにロジックは同じなのに失敗してしまうケースがあります。
そのため、headの内部にscriptを入れるなら、何かしらのオプションが必要になり、それがasync/deferというBoolean Attributesです。
async
<script async src="analytics.js"></script>
(JavaScriptのasync functionとは違います)
input checked
と同じように、宣言するだけで有効になるscript要素のBoolean Attirbutesの一種です。
scriptのダウンロードとHTML Parsingが並列的に行われる一方、DOMContentLoaded eventの場合はDOMContentLoadedの実行を待たず、スクリプトのダウンロードが終わり次第実行されます。
動きはこのようになります。
<head><script></script></head>
とは違い、ダウンロード中にはHTML Parsingを防ぎませんが、実行タイミングはダウンロードされたらHTML Parsingが行われていてもブロックしてスクリプトを実行してしまいます。
そのため、該当のjsコードにてDOM要素の操作ロジックが含まれていると、コードの実行結果が失敗する可能性があります。
一方、DOM要素を操作するのではなく、広告・Third Party Scripts・トラキングの場合は例外的にソースコードがダウンロードされる次第に実行された方が良いので、そちらに関してはasyncが有効です。
そのため、GTAGなどのコードではscript asyncで定義されていること、何かご覧になった方が結構いらっしゃるかもしれません。
特記(async)
[1] Inline Scriptで定義すれば、asyncのプロパティは効果ありません。
Warning: This attribute must not be used if the src attribute is absent (i.e. for inline scripts) for classic scripts, in this case it would have no effect.
<!-- Inline Scriptでは効果なし! -->
<head>
<!-- 省略 -->
<script async>
const h2 = document.getElementById("h2-test")
console.log(h2)
</script>
</head>
[2] 非同期という名前のように、script asyncが順次に定義されると、ダウンロードされたスクリプトから実行されます。
<script async src="A.js"></script>
<script async src="B.js"></script>
この場合、B.jsの容量がA.jsより小さく、ダウンロードが先に終わるとB→Aの順番に実行されます。
defer
こちらもasyncと同じように、script要素のBoolean Attirbutesの一種です。
HTML Parsingとスクリプトのダウンロードが並列的に行われ、スクリプトの実行もHTMLのParsingが完全に終わってから実行されます。HTML Parsingとローディングをブロックせず、DOMが完全に読まれてから(DOMContentLoadedイベント実行前)スクリプトが実行されるようになります。
スクリプトのダウンロードにおいてHTML Parsingもブロックせず、Parsingが終わればすぐスクリプトが実行されるため、DOM要素を操作するロジックがあるときにはパフォーマンス面でも一番優秀です。
なので、ある方は
The best thing to do to speed up your page loading when using scripts is to put them in the head, and add a defer attribute to your script tag:
<script defer src="script.js"></script>
「一旦defer入れてheadに入れてね!」のようにおすすめをされたりしています。
https://flaviocopes.com/javascript-async-defer/
特記(defer)
- asyncと同様に、srcなしのインラインコードでは無効です。
Warning: This attribute must not be used if the src attribute is absent (i.e. for inline scripts), in this case it would have no effect.
The defer attribute has no effect on module scripts — they defer by default.
<!-- Inline Scriptの場合は効果なs -->
<script defer>
const h2 = document.getElementById("h2-test")
console.log(h2)
</script>
(太文字のmodule scriptsについては、後日別の内容として扱いたいと思います!とりあえずはinline scriptと、インポート型のClassic Scriptがメインになります。)
- 複数のscript deferが順次に定義されたら、定義された順番で実行されます。
<script defer src="A.js"></script>
<script defer src="B.js"></script>
容量がB.js < A.jsの場合でもA->Bの順番での実行になります。
おまけ:DOMContentLoaded event
addEventListenerのハンドラーで定義できるイベントとして、
HTMLドキュメントのParsingが完全におわり、defer付のscriptまで読み込まれることを待ってから実行されます。
スクリプトコードの場合は、script deferやmodule scriptsを読み終わってからDOMContentLoadedが動きます。
もちろん、asyncやimage要素など、non-blocking要素はこのイベントに影響ありません。
window.addEventListener("DOMContentLoaded", (e) => {})
まとめ
- link(rel="stylesheet" href="style.css")の場合はmetadataなので、headに入れること
- scriptの場合は
- 通常的にはbodyの一番下のところだったが、async/deferプロパティがある場合は適切に使いこなせたらheadに入れるのもアリ
-
<head><script defer src="main.js"></script></head>
が一般的にパフォーマンス的にいいと言われるが、広告・分析などにDOMに影響がなかったり、scriptコードが十分に小さい場合にはasyncもアリ
ただし、ここに乗っている内容が100%一致しないこともありえます!
その時はフィードバックくださいましたら助かります(m__m)
TMI(Too Much Information)
.mjs(module script)の場合は、基本的にdeferプロパティがついているらしいです。
このため、MDNドキュメントでもclassic script
とmodule script
という用語で分けて説明をしていました。
しかし、ここで扱うのには内容がかなり深くなりそうで、「こんなことがあります」程度でしたいと思います。
あとで勉強していき、記事で書けるなら書きたいと思います。
<script type="module">
/* JS Code */
</script>
参照
海外コラム
https://flaviocopes.com/javascript-async-defer/
https://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html
公式ドキュメント
https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement/defer
https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement/async
https://javascript.info/script-async-defer
https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event