下記の記事をヒントに色々調査してみた。
現象
下記のようなコードを実行するのに異常に時間が掛かっていた。
$('#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]というパラメータを付け加えてキャッシュを無効化してしまうのである。
"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ファイルは静的に参照したほうがいいよ。