読者です 読者をやめる 読者になる 読者になる

tohokuaikiのチラシの裏

技術的ネタとか。

Confluenceのプラグイン開発を承ります。ご連絡はこちらのホームページからお願いいたします。

JavaScriptの最適化について、code.google.comの記事の適当訳

GoogleがWeb全体のスピードアップにいよいよ本格的に着手, 一社だけではできないと強調
からリンクのあった、
http://code.google.com/intl/ja/speed/articles/optimizing-javascript.html
が日本語かと思ったら日本語じゃなかった・・・・。



いやー、意外とというか文字列については、全然知らんかった。

Closureって便利だし、「おぉ〜俺って使ってるジャン」みたいな気になれるからついつい使っちゃうんだけど、高コストなのね・・・・。反省。


ということで、超適当翻訳。どっかの誰かが書いてるかも。

前おき

著者: Google Chromeのエンジニア Gregory Baker, Software Engineer on GMail & Erik Arvidsson
推奨される経験:JavaScriptの実践的な知識

クライアントサイドのスクリプトは、Webアプリを動的にできますが、そのブラウザの実装は非効率になりうるし、パフォーマンスはクライアントによって異ります。ちょっとしたJavaScriptの実行効率を上げる方法について議論します。


文字列について

文字列の結合はI.E6/7のガベージコレクション時のパフォーマンス大きな問題を起こします。I.E8はこの問題に取り組んでいます。文字列の結合の問題ははIE8と他のIE以外の(Chromeとか)のブラウザではあまり差はなくなります。といっても、いまの多くのユーザーがI.E6/7を使っていて、それは文字列を作成する時に十分気をつける必要があるということです。

サンプルです。

var veryLongMessage =
'This is a long string that due to our strict line length limit of' +
maxCharsPerLine +
' characters per line must be wrapped. ' +
percentWhoDislike +
'% of engineers dislike this rule. The line length limit is for ' +
' style purposes, but we don't want it to have a performance impact.' +
' So the question is how should we do the wrapping?';

上記の文字列結合の代わりに、配列のjoinを使ってみましょう。

var veryLongMessage =
['This is a long string that due to our strict line length limit of',
maxCharsPerLine,
' characters per line must be wrapped. ',
percentWhoDislike,
'% of engineers dislike this rule. The line length limit is for ',
' style purposes, but we don't want it to have a performance impact.',
' So the question is how should we do the wrapping?'
].join();
同様に、ループやifをなどで文字列結合を行うのは、非効率

間違った例:(逐一ループで文字列結合をさせる)

var fibonacciStr = 'First 20 Fibonacci Numbers';
for (var i = 0; i < 20; i++) {
	fibonacciStr += i + ' = ' + fibonacci(i) + '';
}

正しい例:(配列にpushしていって、最後にjoinで結合させる)

var strBuilder = ['First 20 fibonacci numbers:'];
for (var i = 0; i < 20; i++) {
	strBuilder.push(i, ' = ', fibonacci(i));
}
var fibonacciStr = strBuilder.join('');
Helper Functionで作成したパーツの結果を用いて文字列を作成する場合

テンポラリに使われる長い文字列を格納した変数を使うのを避けるために、配列とか、それ用に作成した関数で長い文字列を作成しましょう。


たとえば、下記のbuildMenuItemHtml_というメソッドは、結合するための文字列をがりがりと作ってくれるものとして、上のやりかただと、文字列を結合するためにその文字列を内部的に持つコトになってしまいます。ます。

var strBuilder = [];
for (var i = 0; i < menuItems.length; i++) {
  strBuilder.push(this.buildMenuItemHtml_(menuItems[i]));
}
var menuHtml = strBuilder.join();

これを

var strBuilder = [];
for (var i = 0; i < menuItems.length; i++) {
  this.buildMenuItem_(menuItems[i], strBuilder);
}
var menuHtml = strBuilder.join();

ってしてやると、結合のために使う無駄な内部的な文字列は必要ありません。

ClassやMethodの定義

次の方法は、よくないやり方です。baz.Barをnewするたびに、新しい関数とクロージャがfooの定義のために作成されてしまいます。

baz.Bar = function() {
  // constructor body
  this.foo = function() {
  // method body
  };
}

好ましい方法は、

baz.Bar = function() {
  // constructor body
};

baz.Bar.prototype.foo = function() {
  // method body
};

とやって、prototypeにfooを定義してしまうことです。

この方法だと、baz.Barをいくら作ってもたった一つのfooが作成されるだけです。クロージャに至っては一つも作られません。


インスタンスのメンバ変数の初期化について

インスタンス変数の宣言と初期化をprototypeに、その変数が数値だったり文字列だったりnullだったりの型を一緒に置いておきましょう*1。これはコンストラクタが呼ばれた毎に実行されるムダな初期化を避けることになります。
といっても、これは、初期値がnewするときの引数に依存しない場合に意味があります。

たとえば、

foo.Bar = function() {
  this.prop1_ = 4;
  this.prop2_ = true;
  this.prop3_ = [];
  this.prop4_ = 'blah';
};

とするよりも、

foo.Bar = function() {
  this.prop3_ = [];
};

foo.Bar.prototype.prop1_ = 4;

foo.Bar.prototype.prop2_ = true;

foo.Bar.prototype.prop4_ = 'blah';

としておきましょう。

クロージャには気を付けて

クロージャは強力で便利なJavaScriptの機能ですが、いくつかの欠点もあります。

  1. メモリリークの最も大きな原因
  2. クロージャを作ると、(クロージャを持たない)内部関数を作るより遅くなります。そして、Staticな関数の再利用の方がパフォーマンス的にはかなり得です。

たとえば、

static function. For example:
function setupAlertTimeout() {
  var msg = 'Message to alert';
  window.setTimeout(function() { alert(msg); }, 100);
}

は、次のスクリプトより遅いです。

function setupAlertTimeout() {
  window.setTimeout(function() {
    var msg = 'Message to alert';
    alert(msg);
  }, 100);
}

といっても、このスクリプトは次のスクリプトより遅いです。

function alertMsg() {
  var msg = 'Message to alert';
  alert(msg);
}

function setupAlertTimeout() {
  window.setTimeout(alertMsg, 100);
}

当然ですが、クロージャは変数のスコープ領域を階層化します。この階層化された編集領域をブラウザは解釈しチェックする必要があります。(これがコストが掛かるのです)

たとえば、

var a = 'a';

function createFunctionWithClosure() {
  var b = 'b';
  return function () {
    var c = 'c';
    a;
    b;
    c;
  };
}

var f = createFunctionWithClosure();
f();

fがコールされた時に、変数aはbより遠いところにあり、bはcの変数領域よりも遠いところにあるわけです。

I.Eのくろーじゃについての情報は、http://blogs.msdn.com/ie/archive/2007/01/04/ie-jscript-performance-recommendations-part-3-javascript-code-inefficiencies.aspx を参考にしてください。


withは避けよ

コード中にwithを使うのはやめましょう。これはスコープ領域を変更するので、全体のパフォーマンスに負荷をかけます。他の変数領域を見に行く時に高いコストを掛ける原因になります。

ブラウザのメモリリークを避ける

Webアプリにとってブラウザのメモリリークは共通した問題で、パフォーマンスに多大な影響を与えます。ブラウザのメモリの使用量が増えていくと、ユーザーのパソコン全体のシステムも含めてゆっくりになって行きます。もっともよくあるケースは、C++によって実装されたブラウザのDOMとJavaScriptのDOMの循環参照によって引き起こされます。(e.g. between the JavaScript script engine and Internet Explorer's COM infrastructure, or between the JavaScript engine and Firefox XPCOM infrastructure).

Eventハンドラを付ける際には、それ用のEventシステムを使いましょう

よくある循環参照は、「DOM要素 --> eventハンドラ --> クロージャのスコープ --> DOMが入ってる 」というやつです。要素については、ここのMSDNのブログエントリで詳しいです。この問題を避けるために、よく知られテストされたEventハンドラを付けるEventシステムを使いましょう。たとえば、Google doctypeDojoJQueryです。


付け加えると、インラインで作ったイベントハンドラ(<input onclick=みたいな)は、I.Eにおいて別のタイプのリークを起こす可能性があります。これは循環参照ではなく、内部のテンポラリに使用される匿名スクリプトオブジェクトによるリークです。詳しくは、この「I.Eのリークパターンについての理解と解決方法にある「DOM Insertion Order Leak Model」を読んで、このJavaScript Kit tutorialを試してみてください。


Expandoプロパティを避ける

Expandoプロパティというのは、JavaScriptによってDOMに付けられた自由なプロパティのことです。DOMにもともと存在しないプロパティも全くエラーを起こすことなくJavaScriptから付与できます。しかし、これが循環参照の原因になります。Expandoプロパティは、通常メモリリークは起こしません。しかし、偶発的にそして簡単にリークを起こします。

パターンとしては「DOM要素 --> Expandoプロパティ --> 何か媒介するObject --> DOM要素」という感じです。これを避ける方法はもう使わないことにつきます。もし、どうしてもつかいたいなら、プリミティブなタイプのもののみを使うようにしてください。プリミティブでない値を使用する場合は、そのDOMが要らなくなったら明示的にExpandoプロパティも消すことです。「I.Eのリークパターンについての理解と解決方法にある「DOM Insertion Order Leak Model」の「Circular References」に詳しいです。

*1:rather than refernceって何?