はじめに
CSSやJavaScriptなどの読み込み方法(記述)によってはレンダリングを妨げることとなります。
本記事ではJavaScriptの読み込み(記述)に焦点を当てて、レンダリングブロックの対策について説明します。
レンダリングブロック対策の基本はHTMLをとにかく速く出力することです。
この前提をもとに、対策方法について説明できればと思います。
レンダリングの基本
ブラウザにおけるレンダリングの流れは大まかに以下のようになります。
- HTMLをパース。
- DOMツリーを構築する。
- CSSのマークアップからCSSOMツリーを構築する。
- JavaScriptを実行する。
- DOMとCSSOMを組み合わせてレンダリングツリーを構成する。
- 各ノードのビューポート内での正確な位置とサイズをレイアウトする。
- 各ノードを画面に描画する。
ページにアクセスしてからデータを読み込んで描画するまでの処理手順をクリティカルレンダリングパスといいます。このフローを最適化することがレンダリングブロックへの対策となり、JavaScriptの読み込み方ひとつで最適化できます。
では具体的にどういったことに気をつければ良いのか、3つに分けて紹介します。
- 推奨パターン
- アンチパターン
- ケースバイケース
推奨パターン
async・defer属性で読み込み
記述に属性がない場合、途中でリソースを見つけるとパースを中断しリソースの読み込み・実行を行います。(これが所謂レンダリングブロックです。)
async・defer属性を付けることで非同期で(HTMLのパース/DOM構築を阻害することなく)読み込めます。
ただし非同期で行われるのは読み込みのみで、リソースの実行はパースが中断されます。
async・deferの違いはJavaScriptの実行タイミングにあります。
async
:リソースが読み込まれた直後に実行されます。defer
:HTMLパース完了後(DOMContentLoadedの直前)に実行されます。
async属性の特徴
<head>
<script src="foo.js" async></script>
<script src="bar.js" async></script>
</head>
<body>...</body>
- 非同期で読み込まれたあと即座に実行されます。読み込まれたものから実行されるため順番が担保されません。
- トラッキングコード/計測タグなど、他のJavaScriptに依存しないファイルの読み込みに使用。
</body>
直前の読み込みでは非同期の利点が活かされないため、<head>
内で読み込むこと。
defer属性の特徴
<head>
<script src="foo.js" defer></script>
<script src="bar.js" defer></script>
</head>
<body>...</body>
- 非同期で読み込まれ、HTMLパース完了後(DOMContentLoadedの直前)に実行されます。記載している順番に実行されます。
- DOMに依存する、他のJSに依存するファイルの読み込みに使用。
</body>
直前の読み込みでは非同期の利点が活かされないため、<head>
内で読み込むこと。
アンチパターン
インラインJS
<head>
<script>
setTimeout(function() {document.body.appendChild(document.createElement("div")}, 1000);
</script>
</head>
<body>
<p>テキスト1</p>
<script>
setTimeout(function() {document.body.appendChild(document.createElement("div")}, 1000);
</script>
<p>テキスト2</p>
</body>
推奨しない理由
<script>
タグ以降のDOMの構築をブロックしてしまう。- HTML文書のサイズを肥大化させてしまう。
- ブラウザのローカルキャッシュが効かない。
属性無しで読み込み
<head>
<script src="foo.js"></script>
</head>
<body>
<p>テキスト</p>
</body>
推奨しない理由
<script>
タグ以降のDOMの構築をブロックしてしまう。
</body>
の直前で読み込み
<head>...</head>
<body>
<p>テキスト</p>
<script src="foo.js" async></script>
</body>
推奨しない理由
<script>
タグ以降のDOMの構築をブロックしてしまう。- async・defer属性を付けても、パース終了間近に読み込みを開始するため非同期の利点が活かされない。
async・defer属性を両方指定
<head>
<script src="foo.js" async defer></script>
</head>
<body>...</body>
推奨しない理由
- syncに対応していないブラウザはdeferで読み込む記述方法だが、現在そんなブラウザはない。
link rel preloadで読み込み
<head>
<script src="foo.js"></script>
<link rel="preload" as="script" href="bar.js">
</head>
<body>...</body>
推奨しない理由
- async・deferで代替できる。
- ブラウザに依存する記述(IE・FireFoxでは使えない)。
ケースバイケース
Script-injectedでの読み込み
Script-injectedは外部リソース読み込みのソースコードを出力するインラインJSです。(以下の記述を参照)
Script-injected 実行前
<head>
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXX');</script>
</head>
<body>...</body>
Script-injected 実行後
<head>
<script async src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXX"></script>
</head>
<body>...</body>
推奨しない理由
- 非同期読み込みを実現できるが、インラインJSのため基本は使用しない。
- Google Tag Managerなどはこのような仕組みだが、計測イベントのタイミングがシビアでなければ外部ファイルにしてasync・defer属性での読み込みも検討するとベター。
まとめとポイント
- async・defer属性を使用して
<head>
内で読み込む。(deferは実行順序を保証するが、asyncは保証しない) - インラインJSを記述しない。
- Google Tag Managerなどトラッキングコード/計測タグはケースバイケース。
- JavaScriptは複数ファイルに分けずに出来る限り結合・圧縮する。(肥大化しすぎるのも問題なので適宜調整する)
参考URL
フロントエンドのパフォーマンスを徹底解説!ブラウザの気持ちで理解するHTML/Javascript/CSSの話
https://techblog.raccoon.ne.jp/archives/53180280.htmlrel=”preload”を極めるために必要な2種類のプリロード機能
https://techblog.raccoon.ne.jp/archives/1575956867.htmlJavaScript ファイルの読み込みと実行のタイミングを調べる(async, defer)
https://misc.laboradian.com/js-async/1/