JavaScriptがページの読み込み時間を遅くする理由トップ10

CSS Sprites 化や画像サイズの最適化、Data URI 化、CSS/JavaScript ファイルの圧縮・結合・読み込み順番の整理やキャッシュ制御など、本サイトでは主に HTTP リクエストの数、データ量およびそれらの順番に関して色々なテクニックを試してきましたが、さらに深く掘り下げるには JavaScript の実行がページの読み込み時間に与える影響を知っておく必要があると思います。

ウェッブサイトの表示速度を測定するフリーツール集」 でも紹介しましたが、dynaTrace AJAX Edition という優れたツールをフリーで公開している dynaTrace software のブログでこの問題を扱った記事がありましたので共有します。JavaScript 高速化 Tips は (例えば 「JavaScriptを高速化する6つのテクニック」 など) 多々あるかと思いますが、ページ全体に渡る問題を系統的に解説しているところはあまり見当たりません。逆に細かな Tips に欠けるところがありますので、「訳者注」として出来る限り補ってみたいと思います。おかしな日本語訳があったらご指摘ください 😉 。

元記事


サーバーサイドのパフォーマンス問題に焦点を当てた Top 10 Performance Problems に触発され、dynaTrace AJAX Edition のユーザーと共に見てきたWeb2.0アプリケーションにおけるクライアントサイドの問題トップ10を書くに至りました。

症状:JavaScriptが他リソースをブロックし、ページの読み込み時間が遅い

JavaScript によって Web2.0 アプリケーションは幕を開けました。今や JavaScript はどのサイトでも使われているので、古典的な性能解析方法ではページの読み込みや表示を遅くしている原因を見つけることが難しくなっています。多くのブラウザに付属するパフォーマンス解析ツールは、ネットワーク上のウォーターフォールダイアグラムをサポートしています。それらのツールは、どのリソースがダウンロードされたか、またそれら全てのリソースがダウンロードを終えるのにどのぐらいの待ち時間があったかを示してくれます。

Web2.0 アプリケーションにおいては、ページ時間を左右する要因はもはやネットワークだけではありません。我々はダウンロード間に存在する、空白期間に着目しました。これらの空白は通常、ブラウザが全てのネットワーク作業を止めて JavaScript を実行している時間であると説明されています。

JavaScriptの実行による空白期間のあるウォーターフォールダイアグラム ©dynaTrace software®

JavaScriptの実行による空白期間のあるウォーターフォールダイアグラム ©dynaTrace software®

dynaTrace AJAX EditionGoogle Speed Tracer などのツールは、ブラウザがページを読み込むまでの間に実行される JavaScript や DOM アクセス、レンダリング作業などの分析と同様に、ネットワークからのダウンロードをブロックしているこれらの空白期間を分析することが出来ます。

dynaTraceは空白期間に何が起きているかを示してくれます ©dynaTrace software®

dynaTraceは空白期間に何が起きているかを示してくれます ©dynaTrace software®

JavaScript ファイル読み込み後の実行により、残りのネットワークリソースのダウンロード停止が発生し、ページの全読み込み時間を遅くしています。

詳細は JavaScript によるブロッキングと実行時間の遅延を引き起こすスクリプト を参照ください。

これらの症状を説明する10の問題

以下は、JavaScript の実行時間とブロッキング症状の多くを説明する問題のリストです。

#1. IEの遅いCSSセレクタ

パフォーマンス問題を引き起こす原因の No.1 は、IE での CSS セレクタ操作の遅さです。Web開発者は jQuery やPrototype などの JavaScript フレームワークが提供する CSS セレクタを探すメソッドを使います。セレクタ要素を探す一般的な方法は CSS クラス名を使うことです。

var element = $(".shoppingcart");

IE6、7 は本来この手の探索方法を持っていません。ですから jQuery/Prototype は純粋な JavaScript を使って全ての DOM ツリーから検索を実施するための繰り返しが必要になります。この繰り返しは、他のブラウザが持っている探索の実装に比べて非常に遅いのです。またこの繰り返しは DOM サイズに重大な影響を与えます。次の図は、単一のページにおいて IE がそれぞれの CSS 検索にかかった実行時間の合計を示しています。このような呼び出しが多数となることで、各ページとも数秒の遅延時間となります。

dynaTraceによる遅いCSSセレクタの解析 ©dynaTrace software®

dynaTraceによる遅いCSSセレクタの解析 ©dynaTrace software®

IE8 は CSS 探索のもっとましな方法を提供しています。この問題を回避するためには、jQuery/Prototype といった JavaScript フレームワークを最新のバージョンにアップグレードすることが重要です。多くのウェブサイトが未だ、IE の最新版の利点(それでさえ他のブラウザより遅い)を利用しない旧バージョンのままです。

訳者注:

TaskSpeedTaskSpeed は閲覧者のブラウザで JavaScript フレームワークの セレクタ に関わる DOM 操作をベンチマークするサイトです。手持ちの IE8 と FF3.6 で比べてみると、IE8 の方が数倍遅いことと、jQuery に関しては新しい方が速いことが分かります。

DOM 操作のベンチマーク総合時間 [msec]
  jQuery Prototype MooTools Dojo YUI
1.3.2 1.4.1 1.6.0.3 1.2.2 1.4.1 1.5.0 2.7.0
IE8 3185 2456 3593 5569 1835 1786 2535
FF3 1596 665 1078 790 494 483 855

また次のサイトには DOM の効率的な定義方法やアクセス方法が説明されていますので、参考にして下さい。

#2. 同一オブジェクトに対する多重のCSS検索

個々の CSS 検索は実行コストがかかります。同一ページ内で同じ探索を何回も実行することは、必要以上に実行時間の増大を引き起こします。同じ探索を繰り返し実行する代わりに、1回の探索の実行結果を変数に保持し、同一ページ内で再利用することを薦めます。

dynaTraceによるCSSセレクタ実行回数の解析 ©dynaTrace software®

dynaTraceによるCSSセレクタ実行回数の解析 ©dynaTrace software®

上の例では ztBucket に対する8回の実行に660msかかっています。これを1回の呼び出しと再利用により、検索の実行時間の合計は80ms程度に削減することが出来るでしょう。

訳者注:

ここでは Ajax Performance Tuning and Best Practices から例を1つ紹介します。

悪い例

var i;
for (i = 0; i < divs.length; i+= 1) {
	divs[i].style.color = "black";
	divs[i].style.border = thickness + 'px solid blue';
	divs[i].style.backgroundColor = "white";
}

良い例

var border = thickness + 'px solid blue';
var nDivs = divs.length;
var ds, i;
for (i = 0; i < nDivs; i += 1) {
	ds = divs[i].style;
	ds.color = "black";
	ds.border = border;
	ds.backgroundColor = "white";
}

#3. 多過ぎるXHR

JavaScript と XmlHttpRequests の組み合わせは、一般に AJAX と呼ばれる技術の基本です。jQuery のようなフレームワークを使えば、サーバーから追加のコンテンツをダウンロードするための AJAX 呼び出しを簡単に実装できます。ページングメカニズムの実装などが良い例です。全てのページを1度にダウンロードする代わりに、最初のページをダウンロードします。そしてユーザーが次ページに移動したときに、AJAX を通してページの更新内容を読み込み、DOM をリフレッシュします。この方法によりページを遷移せず、またブラウザがページの全てを再ロードする必要がなくなります。

動的に情報を得るために、過度の XHR 呼び出しを使うという誤った例を時々見かけます。例えば製品ページに10個の製品がある場合です。開発者は商品情報の詳細を1つ1つ個別に AJAX で呼び出そうとします。これは10個の製品を表示するのに10個の XHR 呼び出しを実行することを意味します。これは勿論機能しますが、当然ながらユーザーが最後の結果を待つまでに10の工程をサーバーに与えることを意味します。サーバーは追加の負荷となる10個のリクエストを処理する必要があります。1つ1つ個別のリクエストを発行する代わりに、ページ上の10個の製品情報を1つにまとめてリクエストすることを推奨します。

より詳細は Ajax Best Practices: Reduce and Aggregate similar XHR calls を参照ください。

#4. 負荷の高いDOM操作

インタラクティブ性の高いウェッブサイトには DOM 操作が必須です。動的にロードされたコンテンツがサイトに追加されるかも知れませんし、ユーザーの好みの変化に応じてウェブサイトの look and feel を変えるために適用されるかも知れません。この様な場合に、新しい DOM 要素を追加するには複数の方法があります。ブラウザや追加された要素数によって、各方法が性能に与える影響はまちまちです。ですから異なる手法(即ち、HTML として要素を追加するとか、個々の DOM 要素を追加するかなど)を解析することと、利用場面に応じた最善の方法を適用することが重要です。

訳者注:

DOM 追加の悪い例、良い例をいつくか紹介します。まずは Tuning Web Performance から。

新規DOMはCSS設定後に追加し、リフローを最小限にする

悪い例

var foo = document.createElement('div');
document.body.appendChild(foo);
foo.innerHTML = 'Hello world';
foo.className = 'hello';

良い例

var foo = document.createElement('div');
foo.innerHTML = 'Hello world';
foo.className = 'hello';
document.body.appendChild(foo);
フラグメントへの追加・修正は、リフローが発生しない

悪い例

document.body.appendChild(createDivElement());
document.body.appendChild(createDivElement());
...

良い例

var frag = document.createDocumentFragment();
frag.appendChild(createDivElement());
frag.appendChild(createDivElement());
...
document.body.appendChild(frag);

また High Performance Ajax Applications からは cloneNode() によるテーブル作成例を紹介します。

クローンで同一要素の作成を効率化する

悪い例

var i, j, el, table, tbody, row, cell;
el = document.createElement('div');
document.body.appendChild(el);
table = document.createElement('table');
el.appendChild(table);
tbody = document.createElement('tbody');
table.appendChild(tbody);

for (i = 0; i < 1000; i++) {
	row = document.createElement('tr');
	for (j = 0; j < 5; j++) {
		cell = document.createElement('td');
		row.appendChild(cell);
	}
}

良い例

var i, el, table, tbody, row, cell, template;
el = document.createElement('div');
document.body.appendChild(el);
table = document.createElement('table');
el.appendChild(table);
tbody = document.createElement('tbody');
table.appendChild(tbody);
template = document.createElement('tr');

for (i = 0; i < 5; i++) {
	cell = document.createElement('td');
	template.appendChild(cell);
}
for (i = 0; i < 1000; i++) {
	row = template.cloneNode(true);
	tbody.appendChild(row);
}

#5. 多過ぎるJavaScriptファイル

多数の JavaScript ファイルがあるページの場合、JavaScript エンジンがそれらのファイルをロードする度に、ブラウザはコンテキストをスイッチしなければなりません。40以上の個別の JavaScript ファイルを使ったウェッブサイトも珍しくはなくなりました。JavaScript エンジンによるコンテキストの切り替えが必要なだけではなく、これらの JavaScript ファイルをダウンロードするためのネットワーク・アクセスの増加が、ページ全体のロード時間に多大な影響を与えます。

この問題の解決策は個々の JavaScript ファイルを少数のファイルに統合することです。これにより、ネットワーク・アクセスと JavaScript によるコンテキストの切り替えを少なくすることが出来ます。

#6. 巨大なDOM

DOM サイズはページ表示の性能に大きな影響を及ぼします。巨大な DOM には次の様な悪影響があります。

  • ブラウザのメモリを消費する
  • トップノードのスタイル変化がより多くの子ノードに影響を及ぼす場合、その操作時間が長くなる
  • 特に IE では、巨大な DOM はクラス名による CSS 参照の効率を悪くする
  • DOM を繰り返しによってアクセスする場合、どんなカスタム JavaScript も遅くなる

詳細は、Optimizing Data Intensive Webpages by Example を参照してください。

訳者注:

Best Practices for Speeding Up Your Web Site では Yahoo.com のタグ数が700個以下である、と威張っています。この個数であれば YSlow の Reduce the number of DOM elements も A ランクでしょう。DOM 総数を調べるにはアドレスバーに以下を入力します。

javascript:document.getElementsByTagName('*').length

Firebug コンソールのコマンドライン では document.getElementsByTagName('*').length です。

#7. イベントハンドラの過度なバインディング

jQuery や Prototype あるいは YUI といったフレームワークは、例えばハイパーリンクといった、あるタイプの DOM 要素に対するイベントハンドラのバインディングを容易にします。DOM 要素へのイベントハンドラのバインディングは次の3つの方法で性能に影響を与えます。

  1. オブジェクトを見つけ出し、イベントマネージャへの登録やハンドラの操作をオブジェクト自身に割り当てる DOM 要素の修正により、バインディングという操作自体が時間を食います。
  2. イベントが発行されるたびに、イベントマネージャはそのイベントが関連する要素を見つけ出し、正しいイベントハンドラを呼び出す必要があります。
  3. イベントハンドラは、異なるページに移動する際、開放される必要があります。これは DOM に関連するメモリリークを避けるために行われます。

次の図は、どの要素が実際のイベントを扱うのかを識別するために、内部イベントマネージャが全ての要素を参照している様子を示しています。

イベントマネージャーがバインドするオブジェクトを見つけるためにコストの高いCSS参照を実行している様子 ©dynaTrace software®

イベントマネージャーがバインドするオブジェクトを見つけるためにコストの高いCSS参照を実行している様子 ©dynaTrace software®

訳者メモ:

Event Delegation について調べる。

#8. 外部サービスの実行による速度低下

多くのページは、広告バナーや Facebook への接続といった外部コンテンツや、(エンドユーザーのモニタリングや情報のレビューなどといった)外部サービスの呼び出しを埋め込んでいます。この様なコンテンツは通常、3rdパーティーの JavaScript ファイル呼び出しを含んでいます。そしてこれら JavaScript ファイルは大抵、過度な CSS 参照や DOM 操作といった、共通のパフォーマンス上の問題を示します。従って3rdパーティーのコンテンツを解析し、サイト性能に重大な影響を与えることが分かった場合、そういった問題を解決するよう3rdパーティーのコンテンツ提供者に働きかけることが重要です。次のスクリーンショットは、エンドユーザーをモニタリングする目的で情報を収集する JavaScript の実行の様子を示しています。ページ訪問のたびに500ms余計にかかり、エンドユーザーのパフォーマンスに影響を与えています。

3rdパーティー製のJavaScriptによる長い実行 ©dynaTrace software®

3rdパーティー製のJavaScriptによる長い実行 ©dynaTrace software®

#9. 過度なビジュアル効果

多数の JavaScript が、動的にポップアップするメニューやアコーディオン効果といった様な、素敵な視覚的効果を提供してくれます。これらのフレームワークの多くは、サンプルページ上では良い動作をしますが、多数の DOM が使われた現実のページでは必ずしもそうならないことがあります。これらの視覚的効果がブラウザの CPU、描画エンジン、および総合的なウェブサイト性能へ与える影響を分析することが重要です。

より詳細な例を Performance Analysis of dynamic JavaScript menus に示しましたので、参照してください。

#10. 行き過ぎたロギングとモニタリング

カスタムか、あるいは3rdパーティー製のロギング&モニタリングのフレームワークは、ユーザー動向の非常に詳細な情報を収集してくれます。これらに共通の問題は、あまりに多くの情報が集めら、それら収集データがサービス提供者に送信される時、ネットワークの上トラフィックだけではなく JavaScript エンジンにも追加のオーバーヘッドがもたらされるということです。例えばあらゆるマウス移動をロギングすることは名案のように見えるかもしれませんが、(あまりに多くのJavaScript実行により)ブラウザが停滞したり、(モニター&ロギングサービスへのあまりに多数の呼び出しにより)サービスが混雑したり、といったことを容易に引き起こすのです。

より詳細な情報を How to Automate Google Analytics Analysis, Combining Analytics with Performance Management Data に示しましたので、参照してください。

Web2.0のパフォーマンス解析にはさらに多くのことがあります!!

これらのリストはまだまだ完全ではありません。ぜひ JavaScript と AJAX について書いている我々のブログ や、Google の Web Performance Best Practices、Yahoo の Best Practices for Speeding Up Your Web Site でより多くの情報をチェックして下さい。 Steve SoudersJohn Resig らが、Webパフォーマンスについて語っていることも読むべきです。また Planet Performance のWebパフォーマンスに関するブログの優れた記事もチェックすると良いでしょう。

JavaScriptがページの読み込み時間を遅くする理由トップ10」への6件のフィードバック

  1. tokkonoPapa

    ちょっと古くて2009年のだけど、Nicholas C. Zakas の講演 Speed Up Your JavaScript を観ました。HTMLCollection オブジェクトやリフローの事も出てきて、久々に勉強し直した感じです。

    返信
  2. でも使い勝手より
    いかに客に広告を見せ付けるかで
    競いあている現状に
    こんなの意味あるのかなあ

    返信
  3. tokkonoPapa

    今より10倍、レスポンスが早くなっても、広告が10倍リッチになって、結局 元の木阿弥 かと。あるいはリッチにしたいから、高速化が必要とも。

    返信
  4. yos

    とても参考になりました。
    近年のWebアプリ開発において
    十分意味ある記事だと思います。

    返信

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です