ブラウザはHTMLを上から順に読みながら、次に取得すべきリソースを判断していきます。その途中で async や defer の付いていない <script> タグに出会うと、処理が止まります。
スクリプトのダウンロードと実行が終わるまで、HTMLの解析は先へ進みません。これが同期スクリプトによるレンダリングブロックです。
アイデアマンズでは通販サイトを中心にサイトスピード改善を手がけています。その現場で、この問題は実装品質の高いサイトでも繰り返し観測されます。実在する国内4サイトの計測値をもとに、仕組みと対処をコードで整理します。
ブラウザがscriptに出会うと何が起きるか
ブラウザはHTMLのパース中に <script src="..."> タグを見つけると、次の順で処理します。
- HTMLのパースを停止する
- スクリプトをダウンロードする
- スクリプトを実行する
- 実行が完了したらHTMLのパースを再開する
<head> 内に置かれた同期スクリプトの場合、CSSの読み込みより早い段階でこれが起きます。スタイルシートのダウンロードとレンダリングツリーの構築が、スクリプトの完了を待つことになります。
外部ドメインからの読み込みでは問題がさらに深刻です。DNS解決とTLS接続の確立が済むまで、ダウンロードそのものが始まりません。接続コストが丸ごとパースのブロック時間に変わります。
実例で確認する4つのパターン
じゃらんニュース:外部CDNの同期jQueryがFCPを1.3秒押し下げた
リクルートが運営するじゃらんニュースでは、<head> 内の最初の <script> で ajax.googleapis.com からjQuery 1.8.3が同期読み込みされていました。
外部ドメインへの接続にはDNS解決とTLS確立が必要で、この待機は約300〜500msにのぼります。その間、HTMLのパースは止まったままです。
解消シミュレーションでは、これらのCDNリソースを同一ドメインからの配信に切り替えました。既存のHTTP接続が再利用される形になります。
| 指標 | 解消前 | 解消後 | 変化量 |
|---|---|---|---|
FCP |
2.0秒 | 0.7秒 | -1.3秒 |
LCP |
2.2秒 | 0.9秒 | -1.3秒 |
総合スコア |
97 | 100 | +3 |
FCP が2.0秒から0.7秒へ、1.3秒短縮されました。head内の最初のスクリプトが外部ドメインへ同期リクエストを発していたことで、ページ全体の描画開始が押し下げられていた構図です。
同サイトにはさらにhead内に15個の同期スクリプトが残っていました。これらをbody末尾へ移動すると、FCP はさらに0.1秒短縮されています。
朝日新聞デジタル:スコア96でもhead内に同期スクリプトが残る
朝日新聞デジタルは観測時点で 総合スコア 96、LCP 0.3秒と高水準でした。それでもhead内に同期スクリプトが3件存在していました。
CSSのパース完了後にこれらが直列で実行され、合計約23msのレンダリングブロックが発生していました。3件をbody末尾へ移動したところ、FCP は0.3秒から0.2秒へ短縮されています。
元の値が小さいため変化幅は0.1秒にとどまります。それでも、実装品質が高いサイトでも同期スクリプトのブロックは残りうる、という点が確認できます。
Hamee本店:1MBの巨大インラインスクリプトでHTMLが肥大化
同期スクリプトの問題は外部ファイルだけではありません。HTMLに直接書き込まれたインラインスクリプトも、パース負荷の観点でボトルネックになります。
Hamee本店では、サイト内検索用の全商品データJSON(約703KB)とフォームビルダー定義JSON(約303KB)がHTMLに埋め込まれていました。合計約1MBが配信され、HTMLサイズは約1.94MBに達していました。
いずれも初期表示には不要なデータです。初期表示に不要な2つを削除したところ、HTMLサイズは1.94MBから1.00MBへ約50%削減されました。LCP は1.2秒から1.0秒へ短縮されています。
高島屋オンラインストア:未依存スクリプトのasync化でCLSが改善
高島屋オンラインストアでは、body末尾の3つのスクリプトが async なしで同期読み込みされていました。いずれも他から依存されていないことを静的解析で確かめ、async を付与しました。
<!-- 変更後: 未依存のスクリプトにasyncを付与 -->
<script src="/sto/common/js/footerFixed.js" async></script>
<script src="/cdn/.../Chart.js/2.1.4/Chart.min.js" async></script>
<script src="/sto/common/js/lib/smartphoto.js" async></script>
CLS は0.383から0.209へ大きく変化し、総合スコア は7ポイント向上しました。async によって実行順序が変わり、レイアウトシフトのタイミングに影響したと考えられます。同期スクリプトの非同期化は、FCPだけでなくページの安定性にも波及します。
非同期化の3つの対処法
defer:パース完了後に順序を守って実行
<!-- 同期(問題のある状態) -->
<script src="app.js"></script>
<!-- deferに変更 -->
<script src="app.js" defer></script>
defer を付けると、ダウンロードをHTMLパースと並行して行い、パース完了後に記述順で実行します。実行順序が重要なスクリプトに向いています。
async:ダウンロード完了次第に実行
<script src="analytics.js" async></script>
async はダウンロード完了次第に実行し、順序は保証しません。解析ツールや広告タグなど、他への依存がないスクリプトに向いています。
head内スクリプトのbody末尾移動
属性の付与が難しい場合でも、<head> 内のスクリプトを </body> 直前へ移すだけでブロックを回避できます。じゃらんニュースや朝日新聞デジタルの例が、この効果を示しています。
インラインスクリプトについては、初期表示に不要な大量データを非同期取得へ切り替えるとHTMLの転送量を抑えられます。
<!-- 検索が必要になったときに初めてデータを取得する -->
<script>
async function initSearch() {
const res = await fetch('/api/products.json')
window.ALL_PRODUCTS = await res.json()
}
</script>
手元のサイトで確認する
Chrome DevToolsのPerformanceタブを開いてページをリロードすると、タイムラインが表示されます。Parse HTML の処理中に Evaluate Script が割り込んでいる箇所は、同期スクリプトによるブロックです。
Lighthouseレポートでは「Eliminate render-blocking resources」の指摘に <script> が含まれていれば、それが対象になります。
まとめ
- head内の同期スクリプトはHTMLパースを止め、その遅延はFCPに直接影響する
- 外部CDNからの同期読み込みはDNS解決とTLS確立の待機を上乗せする。じゃらんニュースではFCPが2.0秒から0.7秒へ改善
- 約1MBのインラインJSONを削除しただけで、Hamee本店ではLCPが0.2秒改善
- 未依存スクリプトのasync化はCLSにも効く。高島屋オンラインストアでは0.383から0.209へ改善
- 対処は属性の付与か位置の変更が基本。スコア96の朝日新聞デジタルでも同じ問題が残っていた
各サイトの詳しい計測条件は、次の記事にまとめています。

