メニューは充実しています。顧客も気に入っています。動作します。しかし、PageSpeed Insightsでストアをテストするたびに、Googleが1つの特定のファイルに異議を唱えます。つまり、メニュースクリプトです。ラベルは「レンダリングをブロックするJavaScript」と表示され、ファイルは120KBで、モバイルのLCPに600ミリ秒を追加しています。これを修正する必要があることはわかっていますが、前回誰かがdeferを使用することを提案したとき、メニューが破壊されてしまいました。ドロップダウンの半分が動作を停止し、モバイルトグルは何もしなくなり、1時間以内にロールバックしました。
これは一般的な問題です。レンダリングをブロックするスクリプトはShopifyストアにおける最も影響力のあるパフォーマンスボトルネックの1つであり、メニューはよく問題の原因になります。しかし、JavaScriptを遅延読み込みすることは、単にスクリプトタグに属性を追加することではありません。間違った方法でやると、メニューは動作しなくなります。正しく行えば、メニューロジックを1行も変更することなく、モバイルのLCPから半秒削減できます。
この記事では、レンダリングブロッキングの実際の意味、asyncとdeferの動作の違い、そしてメニュースクリプトをメニュー機能を壊さずに読み込む方法について説明します。
- レンダリングをブロックするスクリプトは、ダウンロード、解析、実行まで、ページレンダリングをすべて停止させます。
deferはスクリプトを並行してダウンロードしますが、HTML解析が完了するまで実行を遅延させます。asyncは即座にダウンロードして実行します。DOM順序に依存するメニューを破壊する可能性があります。type="module"はデフォルトで遅延し、モダンなJavaScript機能をサポートします。- defer後のメニュー破壊のほとんどは、defer属性自体ではなく、初期化タイミングによるものです。
レンダリングブロッキングの実際の意味
ブラウザが読み込み属性のない<script>タグをHTMLで見つけると、すべてを停止します。HTML解析を一時停止し、スクリプトをダウンロードして解析し、実行してから、ページ構築を再開します。これは同期またはレンダリングブロッキングの動作と呼ばれています。
この動作の背景ロジックは古いですが、合理的です。JavaScriptはdocument.write()を使ってDOMを変更できるため、ブラウザはすべてのスクリプトがページの構造を変更する可能性があり、したがって処理を続行する前に実行する必要があると想定します。
ここに問題があります。メニュースクリプトはdocument.write()を使用していません。ページの残りの部分が解析される前に実行する必要はありません。しかし、ブラウザがそれを知らないため、ブロックしてしまうのです。
Chromeの「Largest Contentful Paintの最適化」に関するドキュメントによれば、レンダリングをブロックするJavaScriptを排除することは、実行できる最も影響力のある改善の1つです。ドキュメントの先頭のスクリプトが400ミリ秒ブロックすると、DOM以下のすべての要素 — ヒーロー画像、見出し、製品グリッド — が400ミリ秒長く待つことになり、レンダリングに遅延が生じます。
3つの読み込み戦略: デフォルト、Defer、Async
ブラウザがスクリプトを読み込む方法は3つあります。
デフォルト(同期)
<script src="menu.js"></script>
ブラウザはHTML解析を停止し、menu.jsをダウンロードして解析、実行してから再開します。この間、レンダリングはすべてブロックされます。
Defer
<script src="menu.js" defer></script>
ブラウザはmenu.jsをHTML解析と並行してダウンロードしますが、HTML文書全体の解析が終わるまで実行しません。複数の遅延スクリプトはHTML内の順序で実行されます。
Async
<script src="menu.js" async></script>
ブラウザはmenu.jsをHTML解析と並行してダウンロードし、ダウンロードが完了するとすぐに実行します。HTML解析がまだ終わっていない場合でも実行されます。複数のスクリプトがasyncを使用している場合、実行順序は予測不可能です。
動作をまとめた表を以下に示します。
| 属性 | ダウンロード | 実行 | HTML解析をブロック | 実行順序 |
|---|---|---|---|---|
| なし(デフォルト) | 解析をブロック | 即座 | はい | 順序通り |
defer |
並行 | HTML解析後 | いいえ | 順序通り |
async |
並行 | 準備完了後すぐ | はい(実行中) | 予測不可能 |
ほとんどのナビゲーションメニューの場合、deferが正しい選択です。スクリプトを順序通り保つことができるため、あるスクリプトが別のスクリプトに依存している場合(例えば、ユーティリティライブラリに依存するメニュースクリプト)に重要です。また、スクリプトが実行される前にDOMが準備できていることを保証します。これは要素をクエリするコードにとって重要です。
Deferが一部のメニューを破壊する理由(および修正方法)
メニュースクリプトタグにdeferを追加し、ページをリロードすると、メニューが動作しません。ドロップダウンが開きません。モバイルトグルは何もしません。deferを削除すると、すべてが再び動作します。何が起こったのでしょうか?
問題はほぼ常に初期化のタイミングです。以下が3つの最も一般的な原因です。
問題1: スクリプトがテーマのJavaScriptが準備完了する前に実行される
多くのShopifyテーマには、グローバルなJavaScriptオブジェクトまたは初期化関数があり、他のスクリプトがそれに依存しています。メニュースクリプトがそのオブジェクトが作成される前に実行されると、失敗します。
例:
// theme.js(最初に読み込まれる)
window.Theme = { utils: { ... } };
// menu.js(2番目に読み込まれ、Themeの存在を期待)
Theme.utils.initDropdown();
menu.jsが遅延されているがtheme.jsが遅延されていない場合、menu.jsはtheme.jsの実行が終わる前に実行される可能性があり、Themeは未定義になります。
修正方法: 両方のスクリプトを遅延させるか、menu.jsが依存関係をチェックしてから初期化するようにします。
if (typeof Theme !== 'undefined') {
Theme.utils.initDropdown();
}
問題2: インラインスクリプトが遅延スクリプトの前に実行される
インラインスクリプト(外部ファイルから読み込まれていないHTML内に直接記述されたJavaScript)は、ドキュメント順序で実行されます。遅延スクリプトはHTML全体の解析後に実行されます。つまり、すべてのインラインスクリプトの後です。
メニューを初期化するインラインスクリプトがあり、メニューライブラリが遅延されている場合、初期化は失敗します。
修正方法: 初期化ロジックを遅延スクリプトに移動するか、インライン初期化をDOMContentLoadedリスナーでラップします。
document.addEventListener('DOMContentLoaded', function() {
if (typeof MenuApp !== 'undefined') {
MenuApp.init();
}
});
問題3: 複数の遅延スクリプトとのレース条件
メニューがユーティリティライブラリに依存しており、両方が遅延されている場合、HTML内に表示される順序で実行される必要があります。しかし、ユーティリティライブラリが非同期で読み込まれている場合(deferの代わりにasyncを使用)、実行順序は予測不可能になります。
修正方法: すべての相互依存するスクリプトにdeferを使用し、asyncは使用しません。asyncを使用する必要がある場合は、明示的な依存関係チェックを追加するか、動的インポートを使用します。
Type=”module”: モダンな代替案
モダンなJavaScriptはES6モジュールをサポートしており、組み込みで遅延読み込みを行います。
<script type="module" src="menu.js"></script>
type="module"のスクリプトは自動的に遅延されます。HTML解析と並行してダウンロードし、HTML解析完了後に実行されます。deferと同じです。また、importとexportをサポートしており、依存関係の管理が明確になります。
メニュースクリプトがモジュールとして記述されている場合、依存関係を直接インポートできます。
import { initDropdown } from './dropdown.js';
initDropdown();
これにより、従来のdeferでの破壊を引き起こすタイミング問題が排除されます。ブラウザはどのスクリプトがどのスクリプトに依存しているかを正確に知っているからです。
欠点: 古いブラウザ(Internet Explorer、古いバージョンのSafari)はモジュールをサポートしていません。しかし、Can I Useによると、2025年時点で世界中のブラウザトラフィックの95%以上がES6モジュールをサポートしています。ほとんどのShopifyストアでは、これは安全です。
Shopify固有の考慮事項
Shopifyテーマはテーマアーキテクチャによってスクリプトをさまざまな方法で読み込みます。最も一般的なパターンでdeferがどのように動作するかを次に示します。
Theme.liquidスクリプトタグ
メニュースクリプトが次のようにtheme.liquidで読み込まれている場合:
{{ 'menu.js' | asset_url | script_tag }}
script_tagフィルターはデフォルトでdeferを追加しません。手動の<script>タグに置き換える必要があります。
<script src="{{ 'menu.js' | asset_url }}" defer></script>
アプリ埋め込みブロック
メニューがアプリ埋め込みとしてインストールされている場合(サードパーティメニューアプリでよくあります)、アプリはスクリプトの読み込み方法を制御しています。自分でdeferを追加することはできません。アプリ開発者に確認するか、既に非同期でスクリプトを読み込むメニューアプリに切り替える必要があります。
Navi+などのツールはデフォルトでdeferでJavaScriptを読み込みます。これが、従来のメニューアプリよりもLCPテストでパフォーマンスが向上する理由の1つです。
セクションレンダリングと遅延スクリプト
Shopify 2.0テーマはセクションを動的に読み込みます。セクションに遅延スクリプトが含まれており、セクションが初回読み込み後にページに追加される場合(例えば、クイックビューまたは無限スクロールのためのAJAX)、スクリプトが予期したとおりに実行されない可能性があります。
修正方法: DOMContentLoadedは初回ページ読み込みのみに使用します。動的に追加されたセクションの場合、カスタムイベントまたはミューテーションオブザーバーを使用して初期化を手動でトリガーします。
Deferが機能するかをテストする方法
遅延スクリプト読み込みをライブストアにデプロイする前に、十分にテストしてください。以下がチェックリストです。
- デスクトップChrome: ストアを開き、DevToolsを開き、ネットワークタブに移動し、リロードして、メニュースクリプトが他のリソースと並行してダウンロードされ、ドキュメントをブロックしていないことを確認します。
- モバイルシミュレーション: DevToolsでモバイルエミュレーション(iPhoneまたはAndroid)に切り替え、ネットワークを「Fast 3G」にスロットルしてリロードします。メニューは引き続き正しく初期化される必要があります。
- クロスブラウザ: Safari、Firefox、Edgeでテストします。モジュールスクリプトはSafariで少し異なる動作をします。
- 実デバイス: 実際のモバイルデバイスで、実際のモバイル接続でストアを開きます。エミュレーションは便利ですが、実際のレイテンシーはデスクトップテストが見逃すタイミングバグを露呈させます。
- 機能チェック: すべてのドロップダウンを開き、モバイルメニュートグルをテストし、すべてのリンクをクリックして、何も破壊されていないことを確認します。
プッシュ前にdeferを有効にする前後の同じページでPageSpeed Insightsテストを実行してください。LCPが大幅に低下し、レンダリング時間をブロックするリソースが減少するはずです。LCPが改善されない場合、スクリプトがボトルネックでない可能性があるか、対処する他のブロッキングリソースがある可能性があります。
Asyncが適切な場合(および不適切な場合)
asyncは、完全に自己完結型で、DOM順序や他のスクリプトに依存しないスクリプトに適切です。例としては、アナリティクストラッカー、チャットウィジェット、一部の広告スクリプトが挙げられます。
ナビゲーションメニューの場合、asyncは危険です。メニューはDOMが準備できている必要があり、テーマユーティリティに依存することがよくあります。また、通常、顧客がページとやり取りする前に実行する必要があります。スクリプトがHTMLがまだ解析されている間に実行されると、まだ存在していない要素をクエリしようとする可能性があります。
メニュースクリプトにasyncを検討している場合、スクリプトは以下を満たす必要があります。
- 他のスクリプトに依存しない
- DOM要素をクエリしない(またはすべてのクエリを
DOMContentLoadedチェックでラップ) - 実行時間が解析をブロックしないほど小さい
ほとんどのメニュースクリプトはこれらの基準を満たしていません。asyncを使用する特定の理由がない限り、deferまたはtype="module"を使用してください。
適切なメニュースクリプト読み込みの外観
Shopifyストアで適切に最適化されたメニュースクリプトは次のようになります。
<script src="{{ 'menu.js' | asset_url }}" defer></script>
または、モジュールを使用している場合:
<script type="module" src="{{ 'menu.js' | asset_url }}"></script>
スクリプトはページの残りの部分と並行してダウンロードされ、DOMが準備完了後に実行され、ヒーロー画像またはその他のLCP要素がレンダリングされるのをブロックしません。
Chrome DevToolsパフォーマンスタイムラインで、ヒーロー画像がメニュースクリプトの実行前にレンダリングされるのを確認してください。メニュースクリプトが最初に実行される場合、まだブロックしています。
ほとんどのストアは、同期から遅延スクリプト読み込みに切り替えるだけで、モバイルのLCPから300〜600ミリ秒削減できます。メニューは引き続き動作します。顧客は違いに気づきません。しかし、Googleは気づきます。何より、ブラウザは気づきます。ページがより速く表示されるようになります。これはすべてのポイントです。
この記事は、より大きなガイド「メニューとLCP: ナビゲーションが最大コンテンツペイントをどのようにブロックするか」の一部です。