読み書きプログラミング

日常のプログラミングで気づいたことを綴っています

JavaScriptパフォーマンスベストプラクティスその2

Nokia Developerより

コアJavaScriptの落とし穴

evalやFunctionコンストラクタの使用を避けること
  • evalやFunctionコンストラクタの使用は、ソースコードを実行可能なコードに変換しなければいけないので、毎回スクリプトエンジンを呼び出すことになる高価な演算である。
  • 加えて、evalの使用時には文字列の文脈を実行時に解釈しなければいけない。
  • Using eval or Function constructor are expensive operations as each time they are called script engine must convert source code to executable code.
  • Additionally, using eval the context of the string has to be interpreted at runtime.

遅い:

function addMethod(object, property, code) {
    object[property] = new Function(code);
}
addMethod(myObj, 'methodName', 'this.localVar=foo');

速い:

function addMethod(object, property, func) {
    object[property] = func;
}
addMethod(myObj, 'methodName', function () { 'this.localVar=foo'; });

withの使用を避けること

便利なように見えるけれども、with構文は余分なスコープを生み、そのスコープの内容はコンパイル時には未知である。

Although seen as a convenience, with construct introduces an extra scope to search each time variable is referenced and the contents of that scope is not known at compile time.

遅い:

with (test.object) {
    foo = 'Value of foo property of object';
    bar = 'Value of bar property of object';
}

速い:

var myObj = test.object;
myObj.foo = 'Value of foo property of object';
myObj.bar = 'Value of bar property of object';
パフォーマンスが重要な関数の内部でtry-catch-finallyを使わないこと

try-catch-finally構文は、catch節が実行される度に実行時に現在のスコープに新しい変数を作る。キャッチされた例外オブジェクトが変数に割り当てられるからだ。
例外処理は頻度が少ないスクリプトの高レベルでするべきだ。例えば、ループの外。
あるいは可能ならばtry-catch-finallyを完全に避けること。

The try-catch-finally construct creates a new variable in the current scope at runtime each time the catch clause is executed where the caught exception object is assigned to a variable.
Exception handling should be done at as high level in the script where it does not occur frequently, for example outside a loop.
Or if possible, avoid try-catch-finally completely

遅い:

var object = ['foo', 'bar'], i;
for (i = 0; i < object.length; i++) {
   try {
      // do something that throws an exception
   } catch (e) {
      // handle exception
   }
}

速い:

var object = ['foo', 'bar'], i;
try {
    for (i = 0; i < object.length; i++) {
        // do something
    }
} catch (e) {
    // handle exception
}
グローバル変数の使用を避けること

関数や他のスコープ内でグローバル変数を参照すると、スクリプトエンジンはそれらを探すのにスコープをくまなく探さないといけない。
グローバルスコープ内の変数はスクリプトのライフタイムを通じて存続する一方で、ローカルスコープではローカルスコープがなくなった時壊される。

If you reference global variables from within function or another scope, scripting engine has to look through the scope to find them.
Variable in the global scope persist though the life time of the script, whereas in local scope they are destroyed when the local scope is lost.

遅い:

var i,
    str = '';
function globalScope() {
    for (i=0; i < 100; i++) {
        str += i; // here we reference i and str in global scope which is slow
    }
}
globalScope();

速い:

function localScope() {
    var i,
        str = '';
    for (i=0; i < 100; i++) {
        str += i; // i and str in local scope which is faster
    }
}
localScope();
パフォーマンスが重要な関数内ではfor-inを避けること

for-inループでは、開始に先立って、スクリプトエンジンが列挙可能なプロパティすべてのリストを作り、重複をチェックする。
ループ内のコードが配列を変更しないなら、ループスコープ内で配列の長さを事前に計算し変数lenに割り当てることが繰り返される。

The for-in loop requires the script engine to build a list of all the enumerable properties and check for duplicates prior the start.
If your code inside for loop does not modify the array it iterates pre-compute the length of the array into a variable len inside for loop scope.

遅い:

var sum = 0;
for (var i in arr) {
   sum += arr[i];
}

速い:

var sum = 0;
for (var i = 0, len = arr.length; i < len; i++) {
   sum += arr[i];
}
文字列の累算スタイルを使うこと

+演算子を使うとメモリに新しい文字列が生成され、連結された値がそれに割り当てられる。この後初めて、結果が変数に割り当てられる。
連結結果の中間変数を避けるには、+=演算子を使って結果を直接割り当てることができる。

Using + operator a new string is created in memory and the concatenated value is assigned to it. Only after this the result is assigned to a variable.
To avoid the intermediate variable for concatenation result, you can directly assign the result using += operator.

遅い:

a += 'x' + 'y';
|<<

速い:

>|javascript|
a += 'x';
a += 'y';
プリミティブ演算は関数コールより速いことがある

パフォーマンスが重要なループや関数内では、関数コールの代わりにプリミティブな演算を使うことを検討すること。

Consider using alternative primitive operation over function calls in performance critical loops and functions.

遅い:

var min = Math.min(a, b);
arr.push(val);

速い:

var min = a < b ? a : b;
arr[arr.length] = val;
setTimeout()やsetInterval()に文字列ではなく関数を渡すこと

setTimeout()やsetInterval()に文字列を渡すと、文字列はevalと同じ方法で評価される。それは遅い。
代わりに匿名関数にコードをラップすること。するとコンパイル時に解釈され最適化される可能性がある。

If you pass a string into setTimeout() or setInterval() the string will be evaluated the same way as with eval which is slow.
Wrap your code into an anonymous function instead so that it can be interpreted and optimized during compilation.

遅い:

setInterval('doSomethingPeriodically()', 1000);
setTimeOut('doSomethingAfterFiveSeconds()', 5000);

速い:

setInterval(doSomethingPeriodically, 1000);
setTimeOut(doSomethingAfterFiveSeconds, 5000);
オブジェクト内の不要なDOM参照を避けること

これをしないこと:

var car = new Object();
car.color = "red";
car.type = "sedan"

より良い方法は:

var car = {
 color : "red";
 type : "sedan"
}
オブジェクトの解決速度を最大化しスコープチェーンを最小化すること

非効率な方法:

var url = location.href;

効率的な方法:

var url = window.location.href;
スクリプトコメントを最低限に保つこと/長い変数名を避けること

特に関数、ループ、配列内では、スクリプトコメントを最低限に保つか完全に避けること。コメントはスクリプト実行を不要に遅くし、ファイルサイズを増やす。例えば、

Keep script comments to a minimun or avoid them altogether, especially inside functions, loops and arrays. Comments unnecessarily slow down script execution and increase file size. For example,

悪いアイデア:

function someFunction()
{
var person_full_name="somename"; /* stores the full name*/

}

良いアイデア:

function someFunction()
{
var name="somename";
}
スコープ外変数にローカル参照を持つこと

関数が実行される時、実行文脈が生成され、ローカル変数すべてを含むアクティベーションオブジェクトが文脈のスコープチェーンの一番前にプッシュされる。
チェーン内で遠いほど、識別子解決が遅い。それはローカル変数が最も速いことを意味する。
頻繁に使われるスコープ外の変数にローカル参照を持つことによって、変数への読み書きがかなり速くなる。
これは特にグローバル変数や他の識別子解決の深い探索に関してはっきりしている。
また、スコープ内変数(var myVar)はオブジェクトプロパティアクセス(this.myVar)より速い。

When a function is executed an execution context is created and an activation object containing all local variables is pushed to the front of the context's scope chain.
Further in the chain, the slower the identifier resolution is, which means local variables are fastest.
By storing local references to frequently used out-of-scope variables reading and writing to variables is significantly faster. This is visible especially with global variables and other deep searches for identifier resolution.
Also in-scope variables (var myVar) are faster than object property access (this.myVar).

遅い:

function doSomething(text) {
    var divs = document.getElementsByTagName('div'),
        text = ['foo', /* ... n ... */, 'bar'];
    for (var i = 0, l = divs.length; i < l; i++) {
        divs[i].innerHTML = text[i];
    }
}

速い:

function doSomethingFaster(text) {
    var doc = document,
        divs = doc.getElementsByTagName('div'),
        text = ['foo', /* ... n ... */, 'bar'];
    for (var i = 0, l = divs.length; i < l; i++) {
        divs[i].innerHTML = text[i];
    }
}

大きなループ内で要素(例えばhead)にアクセスする必要があるなら、ローカル変数化されたDOMアクセスの使用がより速い。

If you need to access an element (e.g. the head) inside a big loop using a localized DOM access ( get in the example) is faster.

速い:

function doSomethingElseFaster() {
    var get = document.getElementsByTagName;
    for (var i = 0, i < 100000; i++) {
       get('head');
    }
}
値を変数にキャッシュすること

再び必要な値をローカル変数にキャッシュすることはインタープリタが繰り返しの仕事をすることを抑制する。
以下の2,3の例はより広い意味で値を変数にキャッシュすることを明らかにすべきである。

Caching values to local variables where ever needed prevents interpreter from doing the repetative job.
Couple of examples below should clarify the caching/storing values to variable in broader sense.

例1) ループ内で計算を実行する前に数学関数を変数にキャッシュすること

Example 1) Caching math functions in variables before executing calculations within a loop

悪い方法:

var d=35;
for (var i=0; i<1000; i++) { 
  y += Math.sin(d)*10; 
}

より良いアプローチ:

var d = 55;
var math_sind = Math.sin(d)*10;
for (var i=0; i<1000; i++) {
  y += math_sind; 
}

例 2)ループ内で使われる時、配列の長さをキャッシュすること

Example 2) Caching/Storing array length when used in loops

悪いアプローチ:

配列arrの長さがループが繰り返される度に再計算される。

The length of the array arr is recalculated every time the loop iterates.

for (var i = 0; i < arr.length; i++) {
    // do something
}

よりよいアプローチ:

よりよい方法は配列の長さをキャッシュすることである:

Better way is to to cache the length of the array:

for (var i = 0, len = arr.length; i < len; i++) {
    // do something
}


一般に、既に一度やった不要な仕事をインタプリタにさせることを避けるべきだ。例えば:スコープチェーンの解決や複数回使われる式の評価値。値を変数にキャッシュすることはそれらの値が複数回繰り返し使われる時だけ意味を持つ、そうでなければ、変数宣言、値の割り当てのオーバーヘッドを生み、一回実行されるだけ。覚えておくこと。

In General we should avoid sending the interpreter out to do unnecessary work once it has already done it once, eg: figuring out the scope chain or the function evaulation values of an expression used more than once.storing/caching values to variable only makes sense if those values are used repetatively or more than once, otherwise we are creating overhead again for declaring a variable, assigning values and then just using only once, so keep in mind

ソース: Variable Performance, slides High Performance Kick Ass Web Apps Video at JSCONF 2009