はじめに (対象読者・この記事でわかること)
この記事は、Web開発に携わる方、Webページの表示速度改善に興味がある方、そしてJavaScriptの基本的な知識をお持ちの方を主な対象としています。特に、サイトのパフォーマンス最適化に課題を感じている方や、ユーザー体験を向上させたいと考えている方に役立つ情報を提供します。
この記事を読むことで、あなたは以下のことを理解し、実践できるようになります。
- WebページにおけるJavaScriptの読み込みが、なぜパフォーマンスに影響を与えるのか。
scriptタグに指定できるasyncとdeferという二つの属性が、それぞれどのようにスクリプトの実行に影響するか。- ページの特性やスクリプトの役割に応じて、
asyncとdeferを適切に使い分ける方法。
Webページは、読み込みが遅いとユーザーにストレスを与え、離脱率を高める原因となります。JavaScriptはリッチなインタラクティブ機能を提供する一方で、その読み込みと実行方法によってはページの表示をブロックしてしまうことがあります。この記事を通して、その課題を解決し、より快適なWeb体験を提供するための具体的な手法を身につけましょう。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。 - HTML/CSSの基本的な知識 - JavaScriptの基本的な構文と概念
Webページの表示速度とJavaScriptの課題
現代のWebサイトにおいて、JavaScriptはインタラクティブな機能や動的なコンテンツを提供する上で不可欠な存在です。しかし、その強力な機能の裏には、Webページの表示速度に悪影響を与える可能性も潜んでいます。
一般的なWebブラウザは、HTMLファイルを上から順に解析し、DOM(Document Object Model)ツリーを構築していきます。この解析中に<script>タグに遭遇すると、ブラウザはそのJavaScriptファイルのダウンロードを開始し、ダウンロードが完了次第、そのスクリプトを実行します。この間、HTMLの解析とレンダリングは一時停止してしまいます。この現象を「レンダリングブロック(Rendering Block)」と呼びます。
特に、JavaScriptファイルが大きかったり、ネットワーク環境が不安定だったりすると、スクリプトのダウンロードと実行に時間がかかり、その間ずっとユーザーは空白の画面を見せられることになります。これはユーザー体験を著しく損ね、ページの離脱率を高める要因となり得ます。
従来のJavaScriptの読み込み方法としては、以下の二つが主流でした。
-
<head>タグ内に<script>を配置: この場合、JavaScriptのダウンロードと実行がすべて完了するまで、HTMLのレンダリングが開始されません。ページのコンテンツ表示が大幅に遅れるため、ほとんどのケースで推奨されません。 -
<body>タグの終了直前に<script>を配置: これは一般的な推奨プラクティスです。HTMLの解析とDOM構築がほぼ完了した後にJavaScriptが実行されるため、ユーザーは少なくともページのコンテンツを早期に閲覧できます。しかし、それでもJavaScriptの実行が完了するまでは、インタラクティブな操作やイベントリスナーの登録ができないため、完全に最適とは言えません。
これらの課題を解決し、JavaScriptのダウンロードと実行がページのレンダリングをブロックしないようにするために導入されたのが、async属性とdefer属性です。これらの属性は、スクリプトの読み込みと実行のタイミングを制御し、Webページのパフォーマンスを大きく向上させる強力な手段となります。
asyncとdefer:それぞれの動作と具体的な使い方
ここでは、async属性とdefer属性がそれぞれどのように動作し、Webページの読み込みにどのような影響を与えるのかを詳しく見ていきます。また、具体的なコード例を交えながら、それぞれの適切な使い方を解説します。
通常のscriptタグの動作をおさらい
まず、asyncやdefer属性を指定しない、通常の<script>タグがどのように処理されるかを確認しておきましょう。
Html<!DOCTYPE html> <html> <head> <title>通常スクリプトの例</title> </head> <body> <h1>通常のスクリプト読み込み</h1> <p>この段落が表示される前にスクリプトが実行されます。</p> <script src="my-script.js"></script> <p>この段落はスクリプト実行後に表示されます。</p> </body> </html>
この場合、ブラウザはHTMLを上から解析し、<script src="my-script.js"></script>に到達すると以下の処理を行います。
- HTML解析を一時停止します。
my-script.jsファイルをダウンロードします。- ダウンロードが完了したら、
my-script.jsを実行します。 - スクリプトの実行が完了したら、HTML解析を再開します。
この動作は、スクリプトのダウンロードと実行が完了するまで、ページのレンダリング(コンテンツの表示)をブロックしてしまうため、「レンダリングブロック」と呼ばれるパフォーマンス上の問題を引き起こします。
async属性の導入
async属性は、スクリプトのダウンロードとHTMLの解析を並行して行います。スクリプトのダウンロードが完了したら、HTMLの解析を一時中断してスクリプトを実行し、実行が完了したらHTML解析を再開します。
Html<!DOCTYPE html> <html> <head> <title>asyncスクリプトの例</title> <script async src="analytics.js"></script> <script async src="ads.js"></script> </head> <body> <h1>asyncスクリプト読み込み</h1> <p>このコンテンツはスクリプトのダウンロード中に表示され始めます。</p> </body> </html>
async属性の動作概要:
- ブラウザはHTMLの解析を続行しながら、同時に
async指定されたスクリプトのダウンロードを開始します。 - スクリプトのダウンロードが完了したら、HTMLの解析を中断し、直ちにスクリプトを実行します。
- スクリプトの実行が完了したら、中断していたHTMLの解析を再開します。
asyncのメリット:
* レンダリングブロックの回避: スクリプトのダウンロード中にHTMLの解析がブロックされないため、ページの表示が高速化されます。
* 高速なスクリプト実行: ダウンロードが完了次第すぐに実行されるため、他のスクリプトやHTMLの解析完了を待つ必要がありません。
asyncのデメリット:
* 実行順序が保証されない: 複数のasyncスクリプトがある場合、ダウンロードが完了した順に実行されるため、コードに書かれた順番通りの実行は保証されません。これにより、スクリプト間に依存関係がある場合に問題が発生する可能性があります(例:jQueryを先に読み込む必要があるスクリプトなど)。
* DOM構築中に実行される可能性: HTMLの解析が完了する前にスクリプトが実行される可能性があるため、スクリプトが特定のDOM要素にアクセスしようとしたときに、まだその要素がDOMに存在しない、という状況が発生し得ます。
asyncのユースケース:
* Google Analyticsなどの独立した分析スクリプト。
* 広告スクリプトやソーシャルメディアのウィジェットなど、ページの主要機能に影響を与えないサードパーティースクリプト。
* 実行順序が重要ではなく、かつDOMに依存しないスクリプト。
defer属性の導入
defer属性もまた、スクリプトのダウンロードとHTMLの解析を並行して行います。しかし、asyncと異なり、スクリプトの実行はHTMLの解析がすべて完了した後(正確には、DOMContentLoadedイベントの直前)に行われます。さらに、複数のdeferスクリプトがある場合、それらは記述された順番に実行されることが保証されます。
Html<!DOCTYPE html> <html> <head> <title>deferスクリプトの例</title> <script defer src="library.js"></script> <script defer src="main-app.js"></script> </head> <body> <h1>deferスクリプト読み込み</h1> <p>このコンテンツはスクリプトのダウンロード中に表示され始め、全てのスクリプト実行前に完全なDOMが利用可能になります。</p> </body> </html>
defer属性の動作概要:
- ブラウザはHTMLの解析を続行しながら、同時に
defer指定されたスクリプトのダウンロードを開始します。 - HTMLの解析がすべて完了するまで、スクリプトの実行は待機します。
- HTMLの解析が完了した後、スクリプトはHTMLに記述された順序で実行されます。この実行はDOMContentLoadedイベントの直前に行われます。
deferのメリット:
* レンダリングブロックの回避: asyncと同様に、スクリプトのダウンロード中にHTMLの解析がブロックされません。
* 実行順序の保証: 複数のdeferスクリプトがある場合でも、HTMLに記述された順番通りに実行されるため、スクリプト間の依存関係を管理しやすいです。
* DOMへのアクセス保証: スクリプトが実行される時点では、DOMツリーが完全に構築されているため、スクリプト内でDOM要素にアクセスする処理を安全に記述できます。
deferのデメリット:
* asyncと比較すると、スクリプトの実行が遅れる可能性があります(HTML解析完了まで待つため)。
deferのユースケース:
* DOM操作を行う主要なスクリプト(例:イベントリスナーの登録、要素の動的な追加/変更)。
* jQueryのようなライブラリと、それに依存するアプリケーションスクリプトの組み合わせ。
* Webサイトのコア機能を提供するスクリプト全般。
asyncとdeferの使い分け
どちらの属性を使うべきかは、そのスクリプトがページのどの部分に影響を与え、他のスクリプトとどのような依存関係を持つかによって決まります。
| 属性 | HTML解析 | ダウンロード | 実行 | 実行順序 | DOMアクセス | ユースケース |
|---|---|---|---|---|---|---|
| なし | 停止 | 停止 | 実行 | 記述順 | 安全 | 小規模で最速実行が必要な場合(非推奨) |
async |
継続 | 並行 | ダウンロード完了後すぐ中断して実行 | 保証なし | 不確実 | 独立した分析・広告スクリプトなど |
defer |
継続 | 並行 | HTML解析完了後、記述順に実行 | 保証あり | 安全 | DOM操作、依存関係のある主要スクリプト |
選択のポイント:
-
スクリプトが他のスクリプトに依存しているか、または他のスクリプトから依存されているか?
- 依存関係がある →
deferを使用すべきです。実行順序が保証されるため、予期せぬエラーを防げます。 - 依存関係がない →
asyncを検討してください。
- 依存関係がある →
-
スクリプトがDOMを操作するか?
- DOM操作が必要 →
deferを使用すべきです。スクリプト実行時にはDOMが完全に構築されているため、安全にDOMにアクセスできます。 - DOM操作が不要 →
asyncを検討してください。
- DOM操作が必要 →
-
スクリプトの実行がページの表示にどれくらい重要か?
- ページのコア機能であり、DOM操作を伴う →
defer。 - 独立した機能(分析、広告など)で、ページの読み込みをブロックしたくない →
async。
- ページのコア機能であり、DOM操作を伴う →
両方指定した場合:
もし<script async defer src="script.js"></script>のように両方の属性を指定した場合、async属性が優先されます。ただし、古いブラウザがasyncをサポートしていない場合、deferがフォールバックとして機能する可能性があります。現代のブラウザではほとんど意味がないか、意図しない挙動になる可能性もあるため、通常はどちらか一方のみを指定すべきです。
ハマった点やエラー解決
JavaScriptの読み込み最適化において、よく遭遇する問題とその解決策をまとめます。
-
DOM要素へのアクセスエラー:
- 問題:
async属性を指定したスクリプト内で、まだHTMLが完全に解析されていないDOM要素にアクセスしようとすると、「Cannot read properties of null (reading 'style')」のようなエラーが発生することがあります。 - 原因:
asyncスクリプトはHTML解析と並行してダウンロードされ、ダウンロード完了次第すぐに実行されます。この時、まだ必要なDOM要素が構築されていない場合があるためです。 - 解決策:
- そのスクリプトがDOM要素に依存している場合は、
asyncではなくdeferを使用してください。 - どうしても
asyncを使用したい場合は、スクリプト内でDOMContentLoadedイベントをリッスンし、DOMが完全にロードされてから処理を実行するようにします。javascript document.addEventListener('DOMContentLoaded', () => { // DOM要素にアクセスする処理 const myElement = document.getElementById('myElement'); if (myElement) { myElement.style.color = 'blue'; } });
- そのスクリプトがDOM要素に依存している場合は、
- 問題:
-
スクリプト間の依存関係エラー:
- 問題: 複数の
asyncスクリプトがあり、一方のスクリプトがもう一方のスクリプトで定義された変数や関数に依存している場合、実行順序が保証されないため、依存元のスクリプトがロードされる前に依存先のスクリプトが実行されてしまい、エラーが発生することがあります(例:jQueryの前にjQueryに依存するプラグインが実行される)。 - 原因:
asyncはダウンロード完了順に実行されるため、記述順序は無視されます。 - 解決策:
- 依存関係のあるスクリプト群には
deferを使用してください。deferは記述された順に実行されることが保証されます。 - 依存関係が複雑な場合は、WebpackやParcelなどのモジュールバンドラを使用して、スクリプトを一つにまとめ、依存関係を適切に解決することを検討してください。これにより、読み込みを効率化し、依存関係の問題を根本的に解決できます。
- 依存関係のあるスクリプト群には
- 問題: 複数の
-
インラインスクリプトの扱い:
- 問題: HTML内に直接記述されたインラインスクリプト(
<script> console.log('Hello'); </script>)にはasyncやdefer属性を適用できません。これらの属性は、src属性を持つ外部スクリプトにのみ有効です。 - 解決策:
- インラインスクリプトは、基本的にレンダリングブロックを引き起こす可能性があります。可能であれば、外部ファイルに分離し、
asyncやdeferを適用することを検討してください。 - どうしてもインラインで記述する必要がある場合は、その内容が非常に小さく、ページの表示に与える影響が少ないことを確認してください。また、DOMにアクセスする処理は、
DOMContentLoadedイベントを待つなどの工夫が必要です。
- インラインスクリプトは、基本的にレンダリングブロックを引き起こす可能性があります。可能であれば、外部ファイルに分離し、
- 問題: HTML内に直接記述されたインラインスクリプト(
これらのハマりポイントを理解し、適切な解決策を適用することで、JavaScriptの読み込みと実行を最適化し、ユーザーに快適なWeb体験を提供することができます。
まとめ
本記事では、Webページのパフォーマンス改善に不可欠なJavaScriptのscriptタグにおけるasyncとdefer属性について、その動作原理と適切な使い分けを解説しました。
- 通常の
scriptタグはHTML解析をブロックし、レンダリング速度に悪影響を与える可能性があります。 async属性はスクリプトのダウンロードとHTML解析を並行させ、ダウンロード完了次第実行します。高速ですが、実行順序は保証されず、DOM構築前のアクセスには注意が必要です。独立した分析スクリプトなどに適しています。defer属性もスクリプトのダウンロードとHTML解析を並行させますが、スクリプトの実行はHTML解析が完了した後、記述された順序で行われます。DOMへの安全なアクセスと実行順序の保証が必要な、主要なアプリケーションスクリプトに適しています。
この記事を通して、あなたはWebページのロード速度を最適化し、ユーザー体験を向上させるための具体的な知識とテクニックを得られたことと思います。これらの知識を活かすことで、より高速で快適なWebサイトを構築できるようになります。
今後は、WebPackやRollupなどのモジュールバンドラと組み合わせたより高度なJavaScript最適化、HTTP/2やWeb Workersを活用したさらなるパフォーマンス向上戦略についても記事にする予定です。
参考資料
- MDN Web Docs: script - HTML:
<script>要素 - Google Developers: JavaScript の実行を遅らせる
- Webサイト高速化の定番!JavaScriptのdeferとasyncの動作の違いと使い方を理解しよう | 株式会社グランフェアズ