HTMLscript要素に設定できる属性とその作用
async
crossorigin
defer
DOMContentloaded
hthl
integrity
module
nonce
referrerpolicy
script
type属性
データブロック
ここ最近のWebサイトにおいて、Javascriptを使用していないサイトはまずありません。
注: 阿部寛のホームページは除きます。
javascriptを読み込ませるにあたって、script要素の位置も色々と変わってきた歴史があります。
そんなscript要素を正しく使うために、
script要素に設定できる属性について解説していきたいと思います。
script要素について
今回、属性についての話になるので、
JSファイル読み込み、インラインスクリプトの違いに関してはあまり触れません。
属性の効果・作用・振舞いについてにスポットを当てていきます。
インラインスクリプトとは下記のような記述で、html上に記述されているもののことです。
<script>
alert('これはインラインスクリプトより実行されています。')
</script>
script要素に使用できる属性
MDNのscript要素のページに記載されているもので、
非推奨、実験的機能を除いたものに絞ってみていきたいと思います。
属性一覧
- src属性
- async属性
- defer属性
- type属性
- nomodule属性
- crossorigin属性
- referrerpolicy属性
- integrity属性
- nonce属性
MDNの記載内容順とは変更して紹介していきます。
- src属性
-
JSファイルを参照・読み込みするための属性
記述例 )<script src="/path/to/index.js"></script>
- async属性
-
この属性はブール型属性と呼ばれ、要素に記述が有る無しで判断され実行されます。
インラインスクリプトには使用できません。
他の属性地との関連で、動作が変わってくる毒性となり、
読み込みに関する解説は後ほど行います。
記述例 )<script src="/path/to/index.js" async></script>
- defer属性
-
この属性はブール型属性と呼ばれ、要素に記述が有る無しで判断され実行されます。
インラインスクリプトに使用することはできなく、全くの効果はないようです。
のちに記載する type属性のmodule設定 と併用しても効果はありません。module設定の効果が優先されるそうです。
こちらの読み込みに関する解説も後ほど行います。
記述例 )<script src="/path/to/index.js" defer></script>
- type属性
- この属性は属性名:typeと属性値を持つことで動作もしくは評価されます。
以前はMIMEタイプが指定されている事がありましたが、今現在では省略でき、あまり設定されていることを見かけません。
text/javascript以外のデータを設定することで、データブロックとして利用できます。
moduleを値として設定ができ、Javascriptモジュールとして扱い実行されます。
defer属性で記載しましたが、defer属性の影響を受けません。
こちらの読み込みに関する解説も後ほど行います。
記述例 )<script src="/path/to/index.js" type="module"></script> <script type="module">...</script>
- nomodule属性
-
この属性はブール型属性と呼ばれ、要素に記述が有る無しで判断され実行されます。
type属性で読み込まれるJavascriptモジュールが動作しなかった場合の代替スクリプトとして読み込ませるための属性になります。
この属性が付与されている かつ ES2015 に対応しているブラウザの場合、
ブラウザ側で実行するべきではないとの判断から動作しません。
記述例 )<script src="/path/to/index.js" type="module"></script> // 上記の type="module" のscript要素とワンセット <script src="/path/to/nomodule-index.js" nomodule></script>
- crossorigin属性
-
設定その1
crossorigin のみを設定、もしくはcrossorigin=""を設定すると、
crossorigin="anonymous"と同じ効果を得られる。
この設定を行うと、
CORS(オリジン間リソース共有、 Cross-Origin Resource Sharing)が通らない(読み込み先のJSファイルを読み込ませてもらえない)時に、
読み込み不許可によるエラーが発生するが、そのエラーの原因がどんなものだったのかを表示するために設定できます。
記述例に記載した内容はすべて同じ動作となります。
記述例 )<script src="https://external-web-site.com/data.json" crossorigin></script> <script src="https://external-web-site.com/data.json" crossorigin=""></script> <script src="https://external-web-site.com/data.json" crossorigin="anonymous"></script>
// crossorigin属性を付与せず、CORSが通らなかった(ブロックされた)時の開発者ツールに表示される文言 Cross-Origin Read Blocking (CORB) blocked cross-origin response https://external-web-site.com/data.json with MIME type application/json. See https://www.chromestatus.com/feature/5629709824032768 for more details. // crossorigin属性を付与して、CORSが通らなかった(ブロックされた)時の開発者ツールに表示される文言(エラー内容) Access to script at 'https://external-web-site.com/data.json' from origin 'https://whyisthis.dev' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. GET https://external-web-site.com/index.js net::ERR_FAILED 200
設定その2
crossorigin="use-credentials"の設定を行うと、
CORSの許可条件にユーザー資格情報(Cookieなどの情報)を含めているため、接続先にユーザー資格情報を送信するための値になります。
本来であれば読み込む際のリソース取得時にはユーザー資格情報は送信しません。
記述例 )<script src="https://external-web-site.com/data.json" crossorigin="use-credentials"></script>
- referrerpolicy属性
-
この属性を設定すると、値によって、接続先にrefferer情報(自身のページURI)をどうするのかを決める事ができます。
この後出てくる用語についても少し解説します。
解説
オリジン:http もしくは https から始まるサイトのドメイン。本サイトであれば https://whyisthis.dev がオリジンになります。
同一オリジン:webサイトのオリジンとsrcに記載されているオリジンが同一である
URI:簡単にいうとブラウザのアドレスバーに表示されているアドレスになります。
プロトコル:通信方式のことで https, http がこれにあたります。
設定値一覧
no-referrer: 送信しません
no-referrer-when-downgrade: HTTPSでない場合には送信しません。
origin: ページのURIではなくオリジンしか送信しません。
origin-when-cross-origin: 異なるオリジンの場合はオリジンのみを、同一のオリジンの場合はページのパスを含め送信します。
same-origin: 同一オリジンの場合は送信しますが、異なるオリジンの際は送信しません。
strict-origin: プロトコルのセキュリティ水準が同等の場合はオリジンのみを送信し、宛先の安全性が下がる場合は送信しません。
strict-origin-when-cross-origin: 同一オリジンの際はURL全体を送信し、そうでなければ strict-origin と同じ動作をします。
unsafe-url: オリジンとパスを送信します。安全ではないので推奨していません。
記述例 )// 異なるオリジンのリソースを読み込み <script src="https://external-web-site.com/index.js" referrerpolicy="xxxxx"></script>
- integrity属性
-
設定されたソースを読み込む際に、integrityに設定されたハッシュ値を使用して、
読み込んだソースとハッシュ値が一致するかをブラウザが判断し、一致しない場合はエラーをコンソールに表示し、
読み込みを中止するという動作を行います。
CDNのjqueryの読み込みを例に取ります。
記述例 )<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script> // integrityのハッシュ値を少し削ってわざとエラーが起こるようにします。 <script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf" crossorigin="anonymous"></script> // エラー文言は下記が表示されます。 Failed to find a valid digest in the 'integrity' attribute for resource 'https://code.jquery.com/jquery-3.6.3.min.js' with computed SHA-256 integrity 'pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU='. The resource has been blocked.
- nonce属性
-
この属性を設定する前に、サーバー側でscript要素を含むhtmlデータを返す際に、
header情報にContent-Security-Policyを含める必要があるそうです。
header情報に設定したデータによってJavascriptのソースもしくはインラインスクリプトの実行許可が決まります。
設定方法はMDNの こちら に詳しく記載されていて、
nonce属性に言及していうと、
header情報にnonce属性に設定する値と同じ物が設定されている物のみが実行許可が下ります。
記述例 )// サーバーレスポンスのheader情報に下記の情報を含める Content-Security-Policy: script-src nonce=20200228 // htmlに記述されているscript要素 // 読み込み・実行される <script src="/path/to/index.js" nonce="20200228"></script> // 読み込み・実行されない // nonceの値がheader情報と違うため <script src="/path/to/index.js" nonce="20200229"></script> // nonceが設定されていないため、そもそも読み込みされない <script src="/path/to/index.js"></script> // インラインスクリプトも例外にもれず、nonceが設定されていないため、実行されない <script>alert('アラート表示');</script>
type属性を使用してscript要素をデータブロックとする
type属性にjavascriptとして解釈されるMIMEタイプ以外を設定することで、
自動的にデータブロックとして認識されるようです。
しかしながら、
application/ld+jsonはGoogleが提唱する構造化データのtype属性値になるので、
使用を控えた方が良さそうです。
記述例 )
// jsonのような形のテキストデータを入れたデータブロック
<script type="text/data" id="myData">
{
data: 'test'
}
</script>
// 上記のデータブロックからデータを取り出す方法(JSON形式のデータではなくテキストとして)
// 抽出したテキストデータをJSONデータとして利用するには、テキスト→JSONへの変換処理を自前で用意する必要あり
document.getElementById('myData').textContent
script要素の読み込みについて
jsファイルの読み込みについて、
何年か前の常識は、head要素内にscript要素を記述して読み込ませると、
HTMLの読み込み、cssファイルの読み込みその他諸々の処理が中断してしまい、
ページ表示されるまでに時間がかかってしまうから、
body要素の閉じタグ直前に記述しましょうという感じが大勢を占めていたと思います。
そこからReactやVueの登場により、
初期テンプレートでのscript要素の読み込みは位置は変わらず、defer属性が付与されるようになりました。
そこからさらに進んでバンドルツールがwebpackからViteに変わったタイミングなのか、
script要素の記述位置がhead要素内に移動し、type="module"が付与される形に変更されていることに最近知りました。
これを機に読み込み位置がどう関係するのか、属性の役割とはということを知るために、
調べて、この内容をまとめることにしました。
Webページが読み込まれるまでの大まかな流れ
script要素の読み込みが及ぼす影響を解説する前に、
ブラウザ上でWebページがどのように読み込まれていて、
読み込まれる中でscript要素はどのように振る舞っているかを知る必要があるため、
Webページがどのように読み込まれるのかの流れをおおまかに解説していきます。
自分にとって、こちらの記事が細かい内容に関しては分かりやすく感じました。
Zenn @akimuu ブラウザレンダリングの仕組み
- 1、HTMLのダウンロード
- サーバーからHTMLのデータを取得する(この時のデータは0,1で表されたデータ)
- 2、HTMLの解析
- HTMLの形に復元(自分達がindex.htmlに記述する形式に戻す作業)
- 3、DOMの構築
- 開発者ツールのElementタブで見るような形にデータ化
- 4、CSSのダウンロード・解析、CSSOMの構築
- 解析中にstyle要素を見つけると内容(インラインであったり外部ファイル)を取得し解析
- 5、JavaScriptのダウンロード・解析
- 解析中にscript要素を見つけると内容(インラインであったり外部ファイル)を取得し解析
- 6、JavaScriptの実行
- 解析が終了したJavascriptの実行
- 7、レンダーツリーの構築
- DOMとCSSOMを紐付けて、Webページとしての体裁を構築
- 8、Layout
- Viewportに対して、レンダーツリーの内容を配置、配置場所はCSSOMに準ずる
- 9、Painting
- レンダーツリーのDOMに対して色付けを行う、画像・背景色・文字色など
この時、2〜5の順番は適宜入れ替わったりすることがあるので、
そこが難しいところでもあったりします。
4、5は2の解析中に見つかると即時に行われてしまいます。
この時Javascriptがダウンロード・解析されているときにHTMLの解析がストップされます。
これをおこなさない為に、以前はbody要素の最後に記述してストップさせないようにしていました。
CSSはダウンロードの際にHTMLの解析をストップするようですが、
CSSの解析の際にはストップさせることはなく、
7、レンダーツリーの構築の時にCSSOMが用意されていなければストップさせるようです。
このことから、
スタイルシートの読み込みはhead要素の中で先に行い、
JSファイルの読み込みはbody要素の最後に記述する
という形に落ち着いていたようです。
この辺りに関しては自分の中でもざっくりとした知識なので、細かいところで間違っている可能性があります。
間違っているところがあり指摘していただけると修正します。
script要素の記述方法
async ,defer ,type="module"を使用し、組み合わせて使う方法としては何も記述しないパターンを含め5パターン存在します。
<script src="/path/to/index.js"></script>
<script src="/path/to/index.js" async></script>
<script src="/path/to/index.js" defer></script>
<script src="/path/to/index.js" type="module"></script>
<script src="/path/to/index.js" type="module" async></script>
1、何も属性なし(ただのJSファイル読み込み)
これは読み込みと同時に優先されてしまい、全てを停めてしまいます。
言うなればこうです。
Scriptingが起こっているときはHTML Parserは完全に止まってしまっています。
parser:HTML解析
fetch:ダウンロード
execution:実行
2、async属性のみあり
JSの実行時のみHTML Parserを止めてしまいます。
ソースのダウンロード時はParserを止めません。
parser:HTML解析
fetch:ダウンロード
execution:実行
3、defer属性のみあり
ソースのダウンロード時はasyncと同様 Parserを止めません。
読み込み→実行の処理が記述されている場合の実行タイミングは、
DOMContentLoadedが発火する直前で実行されます。それまで実行はストップされます。
parser:HTML解析
fetch:ダウンロード
execution:実行
4、type="module"のみ
ソース(モジュールを含めた)のダウンロード時はasyncと同様 Parserを止めません。
読み込み→実行の処理が記述されている場合の実行タイミングは、
DOMContentLoadedが発火する直前で実行されます。それまで実行はストップされます。
parser:HTML解析
fetch:ダウンロード
execution:実行
5、type="module" async属性あり
ソース(モジュールを含めた)のダウンロード時はasyncと同様 Parserを止めません。
JSの実行時のみHTML Parserを止めてしまいます。
parser:HTML解析
fetch:ダウンロード
execution:実行
上記記述時における実際の動作
デモを作成したので、こちらより確認ください。
属性の違いによるファイルの実行タイミングの違いについて
左側がページの表示における処理タイミング(単位:ms)を表し、
右側が属性ごとのJSファイルによる処理を表しています。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Document
<script>
window.timeline = []
</script>
<script src="./head-defer.js" defer></script>
<script src="./head-module.js" type="module"></script>
<script src="./head-module-async.js" type="module" async></script>
<script src="./head-async.js" async></script>
<script src="./head-plane.js"></script>
</head>
<body>
<script src="./body-last-defer.js" defer></script>
<script src="./body-last-module.js" type="module"></script>
<script src="./body-last-module-async.js" type="module" async></script>
<script src="./body-last-async.js" async></script>
<script src="./body-last-plane.js"></script>
<script>
window.addEventListener('load', () => {
const timingKey = [
'responseEnd',
'domContentLoadedEventStart',
'domContentLoadedEventEnd',
'domComplete',
'loadEventStart',
]
const timing = performance.getEntriesByType('navigation')[0]
timingKey.forEach(key => {
timeline.push({timing:timing[key], pos: key})
})
const fragment = document.createDocumentFragment();
console.table(timeline.sort((a, b) => a.timing - b.timing))
timeline.sort((a, b) => a.timing - b.timing)
.map(timingObj => {
const fragTr = document.createElement('tr');
const time = document.createElement('td')
time.innerHTML = timingObj.timing
fragTr.appendChild(time)
const pos = document.createElement('td')
pos.innerHTML = timingObj.pos
fragTr.appendChild(pos)
fragment.appendChild(fragTr)
})
const tableEle = document.createElement('table');
tableEle.appendChild(fragment)
document.body.appendChild(tableEle);
})
</script>
</body>
</html>
const txt3 = 'head-async'
window.timeline.push({
timing:window.performance.now(),
pos: `${txt3} すぐに実行`
})
window.addEventListener(`DOMContentLoaded`, () => {
window.timeline.push({
timing:window.performance.now(),
pos: `${txt3} DOMContentLoadedで実行`
})
})
import { check } from './module1.js'
check('head-module');
export const check = (txt) => {
window.timeline.push({
timing:window.performance.now(),
pos: `${txt} すぐに実行`
})
window.addEventListener(`DOMContentLoaded`, () => {
window.timeline.push({
timing:window.performance.now(),
pos: `${txt} DOMContentLoadedで実行`
})
})
}
globalなtimeline変数(mix.htmlの9行目)に、
各ファイルでの実行タイミングであるパフォーマンス実行時の時間と、
実行ファイルが何で、どのタイミングの実行なのかを登録したのち、
ブラウザのイベントタイミングをtimelineに登録して、
ソートで並べています。
ブラウザの指標となるイベントは以下の通りです。
- responseEnd
- htmlデータのダウンロードが終わったタイミング
- domContentLoadedEventStart
- DOMContentlLoadedが始まったタイミング
- domContentLoadedEventEnd
- DOMContentlLoadedが終わったタイミング
- domComplete
- DOMツリーの構築が完了したタイミング
- loadEventStart
- loadイベントが始まったタイミング
キャッシュクリアされている際の表示についてを見ていきます。
実際のソースの量(記述量)が少ない為、同じように動作する保証は致しません。
あくまで、動作の確認を目的としたものになるので、
実際に試す場合は、効果測定を自身のサイトで行ってください。
type="module",defer はDOMContentLoaded の直前で実行されているのがわかると思います。
そして、DOMContentLoaded で行われる処理は必ず実行されています。
async については、読み込みがParserと並行で行われ、実行タイミングが、DOMContentLoaded を度々追い越しているのが確認できるかと思います。
そして、DOMContentLoaded を追い越している為に、
DOMContentLoaded で実行するはずだった処理が行われないという事象が確認できます。
まとめ
今回このことについて調べて分かったことは、
script要素に対して属性の付与は必須であるが、
script要素の記述場所と使用する属性によってはフォーマンスが落ちるということ、
async属性の使用はイベントの処理タイミングをコントロールしないといけない
ということでした。
今回の結果を受けて、
<script src="/path/to/index.js" defer>,<script src="/path/to/index.js" type="module">はhead要素内に記述する。
かつ、実行タイミングであるDOMContentLoaded はあまり意識しなくても良い。
<script src="/path/to/index.js" async>,<script src="/path/to/index.js" type="module" async>を記述するときは、
処理の実行タイミングのハンドリングが必須であり、
記述箇所に関してはhead要素内がbetterな感じがしました。
注意点
今回の内容に関しては、それぞれの環境によって違いが出てくるので、
一概にこれが正解という内容ではありません。
そしてこれを使用したことによる不利益に対する補償も行っていません。
実際に試し、納得した上で使用していただければと思います。