LoginSignup
40
39

More than 5 years have passed since last update.

JavaScriptプログラミングで性能を気にした書き方をしてみる

Posted at

はじめに

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 = ''一択ですね。

さいごに

今回は気になったプログラミングの仕方を取り上げてみました。
以前知っていた、’こっちのほうが性能がいい’という書き方は今では、
全然そんなことがないという現実を知る良い機会になりました。

今後もこういうちまちまとしたことを重ねて、性能の良いプログラムを書いていきたいものです。
※当然、アルゴリズムを最適にした上で。

40
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
39