JavaScriptのチューニングポイント(続き)
ポイント1:セレクタの指定ではclass属性を極力使わず、id属性にする
HTMLのid属性とclass属性はよく似ており、どちらを使うべきか迷う方も多いと思います。機能的な違いとしては、id属性は1つのHTML内で重複してはならず、class属性は重複を許すという点が異なるのですが、「いちいち重複の有無とか管理するの面倒だから、全部class属性で書いちゃえ」という判断をしている人もいるかもしれません。
<div id="hoge">hogeというid名を重複させてはいけない</div> <div id="hoge">hogeというid名を重複させてはいけない</div> <div class="hage">hageが重複しているけどclass名なので大丈夫</div> <div class="hage">hageが重複しているけどclass名なので大丈夫</div>
しかし、性能面から見るとこれは大きな過ちです。というのも、これら2つの属性は、セレクタの検索性能に大きな差が出るためです。id属性の場合、HTML内に重複値が存在しないことが保証されているため、目的の値を見つけたらそこで検索を打ち切ることができます。
一方、class属性では、目的の値を1つ見つけても、まだ同じ値が残りの部分に存在している可能性があるため、HTMLファイルを全文検索しなければなりません。それにより、HTMLが大きくなるほど両者の検索パフォーマンスの差異が大きくなっていきます。
たとえば、jQueryで$(".foo")と記述すると次のような処理が実行されます。
// セレクタで選択した結果を格納する配列
var ret = [];
// すべてのタグを列挙する
var elems = document.getElementsByTagName("*");
// それぞれについて、クラス名が foo のものを ret に入れる
for(var i = 0; i < elems.length; i++) {
var classes = elems[i].className.split(" ");
if(classes.indexOf("foo") != -1) {
ret.push(elems[i]);
}
}
いったんすべてのタグを列挙してからfooクラスを検索するので、非常に低速となります。セレクタで検索を行う場合は、面倒がらずにid属性を使うようにしましょう。
ポイント2:一覧表示/検索結果画面でDOMによるinnerHTMLを多用しない
JavaScript内部でHTMLを生成するinnerHTMLやinnerTextの機能は、便利なためによく利用されています。
しかし、これもまた負荷の高い処理です。一覧表示や検索結果画面を生成する際に、ループの中で1レコードずつinnerHTMLを生成するような処理を組んでいると非常に遅くなります。これは、1行追加されるたびに画面描画処理が実行されるためです。
これを防ぐため、一覧表示/検索結果のHTMLはなるべくサーバサイドで作成し、クライアントサイドでのinnerHTML生成は1回にとどめるべきです。しかし、これはサーバサイドのアプリケーションにまで手を入れることになるため、修正コストが許容できない場合もあるでしょう。そのような場合、innerHTMLで目的の箇所へ1レコードずつ追加するのではなく、まずDocumentFragmentへ複数のレコードを追加しておき、DocumentFragmentから全レコードを目的の場所のinnerHTMLへ一括追加するという、クライアントサイドに閉じた次善策も考えられます。
次のコードは、1行ずつinnerHTMLへ追加する処理をループさせるという悪例です。これではパフォーマンスを大きく下げてしまいます。
function addItemBox0() {
// innerHTMLへそのまま追加
var box0 = document.getElementById("box0");
for (var i = 0; i < 100; i++) {
box0.innerHTML = box0.innerHTML + "<span>A" + i + "</span><br>"
}
}
このようなコードは、次のようにDocumentFragmentを間に入れることで高速化できます。
function addItemBox1() {
// DocumentFragment
var dfA = document.createDocumentFragment();
dfA.innerHTML = "";
for(var i = 0; i < 100; i++) {
dfA.innerHTML = dfA.innerHTML + "<span>B" + i + "</span><br>"
}
document.getElementById("box1").innerHTML = dfA.innerHTML;
}
なお、既存コードなど、修正コストが高く難しい場合があります。そのようなときには次善策として、クライアントサイドのロジック変更だけで済ませる方法があります。
また、上記の他にCSSでvisibility:hidden;で処理してから完了後に戻すという対策もあります。
ポイント3:1ページ表示の際に情報量を持ち過ぎない
リッチな画面を作ろうとするあまり、初期表示に必要な情報以外も合わせて保持してしまうというのも、性能を無駄に劣化させるアンチパターンです。「右側にある情報をクリックすると、その情報に紐づく情報がクリックイベントとして表示される」「20件のページ送り機能があるにもかかわらず、初期表示の際にすべての情報をDOMに格納して、[次ページ]をクリックした際に表示させる」といった実装がこれに該当します。
DOMとしての要素数が増えれば増えるほどDOM操作が遅くなるので、「1ページ遷移毎にすべての情報を取る」のではなく、1ページ遷移ごとに表示に必要な最小限の情報のみ取得し表示させることが、処理時間の改善につながります。
ポイント4:DOMによるツリー展開やリンクアンカーをやめる
JavaScriptによるツリー展開やリンクアンカーでは、DOMの要素をすべて読み込んでから走査が行われるため、DOMの要素数が多いとパフォーマンスを大きく下げます。
JavaScriptによるツリー展開やリンクアンカーより、id属性指定によるアンカー方式のほうが圧倒的に高速です。動的にページを変更したい場合にはDOMは便利なツールですが、静的な要素まで不必要にDOMで扱うのは「牛刀をもって鶏を割く」というものです。
ポイント5: XMLHttpRequestは配列にして送信する
例えば、1商品ごとにXMLHttpRequest(以下、XHR)のリクエスト発行しているような場合、配列にして複数リクエストを1リクエストにまとめることで、リクエスト回数と通信オーバーヘッドを減らすことができます。特にXHRが多い場合や低速なネットワーク環境下では、体感できるレベルの効果が得られます。
ポイント6:不用意なマウスオーバイベントを使用しない
マウスオーバイベントを多用すると、画面上でマウスを動かした際にCPU使用率が100%に張り付いて応答しなくなるなどの症状がでます。筆者は、「スクロールダウンイベント(下にスクロールさせた場合に表示に必要な情報を取得するイベント)なども実際動いていたが、よくよく確認するとそのイベント自体不要だった」という実装を見かけたことがあります。
業務パッケージのソフトウェアなどを用いた場合、裏でイベントによるアニメーションが大量に走っているケースが多く見られます。クラシックブラウザなどでは、CPUが張り付き、操作がもっさりといった原因となってしまいます。こうした場合には、必須ではないアニメーションを極力削減する必要があります。
以上、JavaScriptに関する6つのチューニングポイントを紹介しました。まとめると、
- 1つずつオブジェクトを処理するのではなく、複数のオブジェクトを一括操作し、オーバヘッドを減らす
- 本当に必要な情報しか表示しない
という節約精神が基本だといえます。
