jQueryの.html()を使うときの注意

下記の記事をヒントに色々調査してみた。

jquery の html() と append を追う

現象

下記のようなコードを実行するのに異常に時間が掛かっていた。

$('#area').html(innnerHtmlText);

Chromeの開発支援ツールを使ってプロファイラやネットワークアクセスを監視すると、ここを実行している間に大量の*.jsファイルをAjaxで取得しており、ここで時間が掛かっているようだった。

調査

jQueryのソースコードを追っていくと、以下のことが分かった。

  • htmlメソッドの引数で与えられるHTMLテキストにscriptタグが入っており、かつ、それが外部のjsファイルを参照している場合はjQueryライブラリがそれをAjaxで取得(して評価)する
  • scriptタグが複数ある場合でも、外部jsファイルはパラレルに取得しない。シーケンシャルに取得する。
  • 外部jsファイルをAjaxで取るとき、キャッシュは効かない

外部jsファイルをAjaxで取る瞬間のコールスタックは以下のようになっていた。

html()→append()→domManip()→_evalUrl()→ajax()

コードを追っていくと、まずdomManip()の中でjsファイルを一つずつ読むforループがある。その中で_evalUrl()が呼ばれる。これは以下のような関数だ。

jQuery._evalUrl = function( url ) {
	return jQuery.ajax({
		url: url,
		type: "GET",
		dataType: "script",
		async: false,
		global: false,
		"throws": true
	});
};

ここで、async:falseが設定されている。なぜfalseにしているかというと、scriptタグはDOMツリーの(深さ優先探索)出現順で評価しなければならないからだろう。取得は非同期に(≒パラレルに)行い、一気にデータを取ったあとで出現順序順にコードを評価、という事も出来なくはないが結構面倒なコードになるはずだし、パフォーマンスにも影響するかもしれない。なので、async:falseにしたのはまあ妥当であると思う。

さらに、重要なのがtype:scriptが設定されていると、cache:trueが設定されない限りはURL末尾に_=[timestamp]というパラメータを付け加えてキャッシュを無効化してしまうのである。

 jQuery.ajax()

"script": Evaluates the response as JavaScript and returns it as plain text. Disables caching by appending a query string parameter, "_=[TIMESTAMP]", to the URL unless the cache option is set to true.

恐らく、動的にjsファイルを読み込まなければならない(htmlソースを動的に設定し、その中にscriptタグが入っている)というケースでは、その読込先のjsファイルが静的に決まっているわけがない、という想定の元なのだと思う。

原因

ではなぜ、今回私が調査したとあるシステムがが遅かったかというと、問題が発生するシステムではhtml()で設定するHTMLテキストの中に大量の外部jsファイルを参照するようなscriptタグがあり、中にはMBのオーダーのファイルもいくつかあった。こんなものをキャッシュも使わずに、しかもシーケンシャルで読み込み、さらに数MBのJavaScriptコードを評価したら、そら重くもなると思う。

教訓

静的なjsファイルは静的に参照したほうがいいよ。