はじめに
JavaScriptを書くようになって早4年。
IE6がー!(T_T)なんて言ってた時代が懐かしく感じます。
最近はモダーンなブラウザの性能がガンガン上がってくれるおかげで、随分とプログラムの書き方を気にしなくて良くなりました。
が、そんな甘ったるい環境に慣れていると、自分のプログラムの性能に対するこだわりが消えてしまう。
ということで、ちょっと気になったプログラムの書き方について性能検証をしてみました。
測定結果の出し方
各種性能の出し方は下記のルールで出しています。
- それぞれの評価対象の実行文の最初と最後の時間の差(ms)を測定する。
- 測定対象は10万回ループ処理した結果
- 10万回ループ処理の結果を10回施行し、その平均値を出す。
- 測定対象のブラウザはIE11, Chrome 47.0.2526.106 m, Firefox 42.0
- 10万回ループを行うデータ及び変数として下記を予め用意。
var i, len, ary = [], /* Date */start, /* Date */end;
for (i = 0, len = 100000; i < len; i++) {
ary.push(i);
}
という感じで行います。
測定
ループの書き方
ループ1
配列に対して、ループ処理を実行するときの性能を気にした書き方を検証。
まずは一般的な下記のコードを見てみましょう。
var div;
start = new Date();
for (i = 0, len = ary.length; i < len; i++) {
div = document.createElement('div');
div.textContent = i;
}
end = new Date();
console.log(end - start);
ループ2
次に下記のソースコードを見てみましょう。
var div;
start = new Date();
ary.forEach(function (val, index, ary) {
div = document.createElement('div');
div.textContent = val;
});
end = new Date();
console.log(end - start);
IE9以降で利用出来るArrayに対するループ処理です。
詳しくは下記を参照。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
では結果を見てみましょう。
ループ1の結果
IE11 | Chrome | Firefox |
---|---|---|
17187ms | 126ms | 127.9ms |
ループ2の結果
IE11 | Chrome | Firefox |
---|---|---|
26508ms | 147.8ms | 127.2ms |
IE\(^o^)/
なんたる悲劇です。
結論としてはIEでは、ループ処理なんかさせんな!ってこと?
と、いうのはちょっと早計です。
どうやら、IE11は、document.createElementに結構なコストが発生する模様。
※測定は割愛します。
ループ処理の結論
結論としては、forEachもlengthを取得してループを回す方法も大差ありませんでした。
chromeではバージョンによってforEachが速くなったり、遅くなったりした事もあります。
また、ソースコード量としてもあんまり変わりません。
慣れてるのを書いていけば良いのかなと思います。
さて、IE11の悲劇で滅入るところですが、次の実験に行きたいと思います。
多階層ハッシュアクセス
次は多階層Objectへのハッシュアクセスについてです。
毎回目的のキーまでのハッシュアクセスを行う
var obj = {key1 : {key2 : {key3 : {key4 : []}}}};
start = new Date();
ary.forEach(function (val, index, ary) {
obj.key1.key2.key3.key4.push(val);
});
end = new Date();
console.log(end - start);
入れ子になったObject(実際にこんなに深いObjectはそうそう無いとは思いますが。)に対するアクセスを、
毎回頑張ってハッシュアクセスを繋いでいく方法。
対するは、下記の方法。
目的のキーへの参照を保持し、その保持した参照に対して処理を行う
var obj = {key1 : {key2 : {key3 : {key4 : []}}}},
ref;
start = new Date();
ref = obj.key1.key2.key3.key4;
ary.forEach(function (val, index, ary) {
ref.push(val);
});
end = new Date();
console.log(end - start);
こちらはkey4までのアクセスを一度変数に参照を代入することによって、
ハッシュアクセスの回数を減らそうとしています。
for (var i = 0, length = some.length; i < length; i++) {
// do something.
}
こういったときにもよくlengthをそのまま使うのではなく代入されることがあります。
さて、どれくらいの性能差が出るのでしょう。
毎回目的のキーまでのハッシュアクセスを行う の結果
IE11 | Chrome | Firefox |
---|---|---|
6.7ms | 7ms | 1.9ms |
目的のキーへの参照を保持し、その保持した参照に対して処理を行う の結果
IE11 | Chrome | Firefox |
---|---|---|
5.5ms | 7ms | 2ms |
ハッシュアクセスの仕方の結論
基本的にはどちらも性能は変わらない。
IE11のみ多階層アクセスへの参照を持ったほうが微妙に早い。
ただ実際にソースコードを書く場合、あんまり参照を増やすようなコーディングは可読性を落とす事につながります。
この程度であれば、可読性をあげるために、目的のキーへの参照を持つだけの変数は書かない、ということで良いと思います。
それでは次に行きます。
document.appendChild
JavaScriptを書いててほぼ発生するのはappendChildを含むDOMへのアクセス。
※Node.jsの方はぜひとも文句は言わずに、ドウゾ。
そのDOMへのアクセスについて、特にappendChildについて測定を行います。
今回はよりわかりやすく性能を見るために、
単一のElementをdocument.body直下にappendしていくのと、
階層構造をもつElementをdocument.body直下にappendする2パターンを測定します。
追加したいElementを都度appendChild
start = new Date();
ary.forEach(function (val, index, ary) {
div = document.createElement('div');
div.textContent = val;
document.body.appendChild(div);
});
end = new Date();
console.log(end - start);
start = new Date();
ary.forEach(function (val, index, ary) {
div = document.createElement('div');
span = document.createElement('span');
document.body.appendChild(div);
div.appendChild(span);
span.textContent = val;
});
end = new Date();
console.log(end - start);
追加したいElementをdocumentFragmentにまとめた最後にappendChild
start = new Date();
fragment = document.createDocumentFragment();
ary.forEach(function (val, index, ary) {
div = document.createElement('div');
div.textContent = val;
fragment.appendChild(div);
});
document.body.appendChild(fragment);
end = new Date();
console.log(end - start);
start = new Date();
fragment = document.createDocumentFragment();
ary.forEach(function (val, index, ary) {
div = document.createElement('div');
span = document.createElement('span');
span.textContent = val;
fragment.appendChild(div);
});
document.body.appendChild(fragment);
end = new Date();
console.log(end - start);
追加したいElementを都度appendChild の結果
測定パターン | IE11 | Chrome | Firefox |
---|---|---|---|
1階層Element | 9867ms | 177.2ms | 326.1ms |
2階層Element | 4421ms | 346.6ms | 542.8ms |
追加したいElementをdocumentFragmentにまとめた最後にappendChild の結果
測定パターン | IE11 | Chrome | Firefox |
---|---|---|---|
1階層Element | 9907ms | 186.7ms | 237.7ms |
2階層Element | 10146ms | 248.7ms | 326.9ms |
appendChildの結論
IE11が、謎な挙動をしています。
測定方法はすべて一緒なので、何故2階層ElementのappendChildで測定結果が短くなったのかは不明です。
が、基本的にはdocumentFragmentを利用する、ということで良いのではないでしょうか。
documentFragmentに詰めるElementの階層が深くなればなるほど性能が改善されていくような感じに見えました。
じゃんじゃん行きましょう。
Create Element
エレメントの作成方法について、3通りやってみます。
今回は下記のような、構造のエレメントを作成する方法として3つ測定してみました。
<div>
<span>[loop counter]</span>
</div>
document.createElementを都度利用する
start = new Date();
fragment = document.createDocumentFragment();
ary.forEach(function (val, index, ary) {
div = document.createElement('div');
span = document.createElement('span');
div.appendChild(span);
span.textContent = val;
});
end = new Date();
console.log(end - start);
jQueryを利用する
start = new Date();
fragment = document.createDocumentFragment();
ary.forEach(function (val, index, ary) {
div = $('<div><span>' + val + '</span></div>');
});
end = new Date();
console.log(end - start);
$()の引数にstringで書かれたHTML構造を代入することで、Elementを生成する方法です。
結構便利なので、利用している方も多いのではないでしょうか。
document.cloneNodeを利用する
start = new Date();
master = document.createElement('div');
master.appendChild(document.createElement('span'));
fragment = document.createDocumentFragment();
ary.forEach(function (val, index, ary) {
div = master.cloneNode(true);
div.children[0].textContent = val;
});
end = new Date();
console.log(end - start);
document.createElementを都度利用する の結果
IE11 | Chrome | Firefox |
---|---|---|
7679ms | 215.8ms | 213.5ms |
jQueryを利用する の結果
IE11 | Chrome | Firefox |
---|---|---|
38581ms | 2687.3ms | 2062.1ms |
document.cloneNodeを利用する の結果
IE11 | Chrome | Firefox |
---|---|---|
7954ms | 326ms | 483.6ms |
Crete Elementの結論
IEはやっぱり(ry
cloneNodeはElementの構造の解析でも入るのか、普通にcreateElementするよりも少し遅い結果になりました。
とは言っても、10万回施行した結果で性能差はほんの少しだけ、ですが。
jQueryはやはり便利なユーティリティを詰め込んでくれているので、初期化に少し時間がかかるようです。
単純にElementを生成するだけというのなら、少々面倒でもcreateElementする、というのが性能的には最適解となりそうです。
ただ、実際にコンポーネントの作成や、Elementの生成を動的に行うプログラムを書いている場合、
予め作ったElementに対してcloneNodeをするやり方が一番便利なのではないかと思います。
性能差に大きな開きがない以上(比率で見ると大きいですが・・・)cloneNodeを利用するのが、
現実的な感じなのではないでしょうか。
次が最後です。
removeChild
innerHTML = ''で一括削除
ary.forEach(function (val, index, ary) {
div = document.createElement('div');
span = document.createElement('span');
document.body.appendChild(div);
div.appendChild(span);
span.appendChild(document.createElement('a'));
});
start = new Date();
document.body.innerHTML = '';
end = new Date();
console.log(end - start);
removeChild(firstChild)
ary.forEach(function (val, index, ary) {
div = document.createElement('div');
span = document.createElement('span');
document.body.appendChild(div);
div.appendChild(span);
span.appendChild(document.createElement('a'));
});
start = new Date();
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
end = new Date();
console.log('remove child 2-2', end - start);
innerHTML = ''で一括削除 の結果
IE11 | Chrome | Firefox |
---|---|---|
374.3ms | 26.2ms | 809.5ms |
removeChild(firstChild) の結果
IE11 | Chrome | Firefox |
---|---|---|
2579.3ms | 56.8ms | 870.7ms |
removeChildの結論
どうやらinnerHTML = ''で良いようです。
http://jsperf.com/innerhtml-vs-removechild/67
リンクのサイトでも検証していますが、innerHTMLにペタッとするのが早いみたいです。
以前はremoveChildしていくほうが速いという結果が出た時期もありました。
ということで、ここは__innerHTML = ''__一択ですね。
さいごに
今回は気になったプログラミングの仕方を取り上げてみました。
以前知っていた、’こっちのほうが性能がいい’という書き方は今では、
全然そんなことがないという現実を知る良い機会になりました。
今後もこういうちまちまとしたことを重ねて、性能の良いプログラムを書いていきたいものです。
※当然、アルゴリズムを最適にした上で。