Mapion マピオンラボ Javascript iPhone Webアプリにおける13のJavaScript高速化アレコレ
iPhone Webアプリにおける13のJavaScript高速化アレコレ
- 2010.06.21
- 中村 浩士
- Javascript
- コメ(2)
- トラバ(0)
![[clip!]](http://parts.blog.livedoor.jp/img/cmn/clip_16_12_b.gif)


こんちは、中村です。
先日マピオンラボよりリリースしたiPad用地図「マピオンタッチ デラックス」のコアとなるフリースクロール地図ライブラリの開発を担当させて頂きました。せっかくなのでターゲットブラウザをMobileSafariに設定し、比較的スペックがプアなiPhone3Gや3GSでも十分な速度で動作するようJavaScript部分の高速化をアレコレ調べてみたのでまとめてみます。一番低スペックなiPhone3Gで実際にどの程度早くなるかをタイマーで計測してみましたってのが本エントリーのミソです。
JavaScriptパフォーマンスチェック - Googleドキュメント
1. new Object より {}(Objectリテラル)
オブジェクトを1万個生成するならばリテラルで記述することで約26ms高速化!
loop=10000;
for(var i=0;i<loop;i++)({}); // 約34ms
for(var i=0;i<loop;i++)new Object; // 約60ms
2010.06.22追記
サンプルコードを以下記述しておりました。
for(var i=0;i<loop;i++){};
これではオブジェクトリテラルではなくただのブロックのため何も処理はなされません。実測したコードでは正しくオブジェクト生成されるコードを書いて計測はしておりましたが、サンプルコードとして記載する際に記述を省略してしまったことにより、まったく意味の異なるコードとしてしまいました。大変失礼致しました。念のため、再度計測した値を記載しておきます。
for(var i=0;i<loop;i++)({}); // 約31ms
for(var i=0;i<loop;i++)new Object; // 約55ms
for(var i=0;i<loop;i++){}; // 約4ms2. new Array より [](Arrayリテラル)
空配列を1万個生成するならばリテラルで記述することで約45ms高速化!ちなみにArrayを生成するコストはObjectを生成するコストの2倍。
for(var i=0;i<loop;i++)[]; // 約88ms for(var i=0;i<loop;i++)new Array; // 約133ms
3. コレクションのアクセスは配列+添字が最速
Objectをキーバリューストアっぽく利用するケースも多いかと思いますが、キーアクセスする必要がない(逐次処理するのみ)であれば配列+添字とするのが高速です。1万回のアクセスで約150ms~280ms以上高速化!キーを添字(整数値)から文字列にするとコストが激増するってのがポイント。
var a=[];
for(var i=0;i<loop;i++)a[i]=i; // 約13ms
for(var i=0;i<loop;i++)a[i+""]=i; // 約280ms
var a={};
for(var i=0;i<loop;i++)a[i]=i; // 約170ms
for(var i=0;i<loop;i++)a[i+""]=i; // 約290ms
4. 配列末尾への要素追加は push より 代入
配列末尾への要素追加はlengthを利用すれば単純な代入で書くことが可能でpushよりも高速です。1万回の要素追加で9msの高速化。
var a=[]; for(var i=0;i<loop;i++)a.push(i); // 約23ms var a=[]; for(var i=0;i<loop;i++)a[a.length]=i; // 約14ms
5. 頻繁に生成するオブジェクトのメソッドはコンストラクタ内ではなくprototypeに
メソッドをコンストラクタ内に関数リテラルとして記述すると、コンストラクタが呼び出される度に関数リテラルが評価されることになります。クロージャーによる変数スコープの解決など特に必要がなければprototype参照にメソッド関数を格納しましょう。そうすれば関数評価は1度で済みます。メソッドを3つ持つオブジェクトを定義した場合、1万回のオブジェクト生成で約600msの高速化!
function FuncLiteral(){
this.a = function(){return "a"}
this.b = function(){return "b"}
this.c = function(){return "c"}
}
for(var i=0;i<loop;i++)new FuncLiteral(); // 670ms
function NoFuncLiteral(){}
NoFuncLiteral.prototype.a = function(){return "a"}
NoFuncLiteral.prototype.b = function(){return "b"}
NoFuncLiteral.prototype.c = function(){return "c"}
for(var i=0;i<loop;i++)new NoFuncLiteral(); // 約75ms
6. 関数呼び出しは極力少なく
JavaScriptでは関数呼び出しによるオーバーヘッドが大きいように感じます。試しにメソッド内で関数呼び出しを1回多く行うメソッドでは、1万回の呼び出しで約30ms差が出ました。
function obj(){}
obj.prototype.callSingle=function(){
return "hello";
}
obj.prototype.callTwice=function(){
return this.func("hello");
}
obj.prototype.func=function(a){
return a;
}
var o = new obj();
for(var i=0;i<loop;i++)o.callSingle(); // 約33ms
for(var i=0;i<loop;i++)o.callTwice(); // 約65ms
関数呼び出しのネストの深さや関数スコープ外の変数アクセス有無などで、オーバーヘッドはさらに大きくもなると考えられます。通常、メンテナンス性や可読性のために関数分割を行いますが、関数呼び出しを減らすためにインライン展開してしまうというのも一つの手かもしれません。
7. Math.absよりも三項演算子
絶対値を求める場合、Mathクラスのabsメソッドを呼ぶよりも三項演算子で解決したほうが高速です。1万回の試行で約18msの高速化!
function abs(){
var a = 100;
return a>0?a:-a;
}
for(var i=0;i<loop;i++)abs(); // 約50ms
function mathAbs(){
var a = 100;
return Math.abs(a);
}
for(var i=0;i<loop;i++)mathAbs(); // 約68ms
8. 単純な数字文字列から数値の変換はプラス単項演算子で
数字文字列から数値を取得する方法はいくつかありますが、数字のみで構成される文字列であればプラス演算子で解決するのが高速です。parseInt呼び出しと比較した場合、1万回の試行で約80msの高速化!
for(var i=0;i<loop;i++)+"0"; // 約26ms
for(var i=0;i<loop;i++)parseInt("0"); // 約106ms
ただし「180px」などの文字列に対してプラス演算子を行うと「NaN」が返ります。そういったケースでは素直にparseIntを利用しましょう。
9. 小数点切捨てにparseIntを利用しない
素直にMath.floorを利用したほうが高速です。1万回の試行で約50msの差が出ます。
for(var i=0;i<loop;i++)Math.floor(i); // 約74ms for(var i=0;i<loop;i++)parseInt(i,10); // 約125ms
10. documentのアクセスはローカル変数に
ビルトインオブジェクトのdocumentにアクセスすることは多いですが、ローカル変数に格納するだけでいくらかの高速化が見込めます。1万回のアクセスで約7msの高速化!
for(var i=0;i<loop;i++)document; // 約11ms var doc = document; for(var i=0;i<loop;i++)doc; // 約4ms
些細な差ですが、この原理として、JavaScriptの変数アクセスはローカル変数が最も早く、外のスコープのアクセスになるにつれて遅くなる、というものがあります。documentは一番外のスコープ(グローバルなwindowスコープ)にあるため、例えば深い関数呼び出し時にはさらなる高速化が見込めます。またdocumentに限らずwindowスコープのオブジェクトや変数にアクセスするスクリプトに対して有効なため、最も意識するべき高速化かもしれません。
11. domオブジェクトのプロパティアクセスに注意しろ!
domオブジェクトのプロパティアクセスはかなりコストが大きいです。例えば要素のレンダリング横幅を取得する「offsetWidth」へのアクセスを1万回行う場合、一度ローカル変数に格納してからアクセスすることで約120ms高速化!
for(var i=0;i<loop;i++)div.offsetWidth; // 約127ms var ow = div.offsetWidth; for(var i=0;i<loop;i++)ow; // 約5ms
12. try~catch~句は遅い
try~catch~句が現れると、スクリプト実行に若干のオーバーヘッドが発生します。ループ内では注意が必要で、ループ全体をtry~catch~で囲んだほうがオーバーヘッドが少なくなるようです。
for(var i=0;i<loop;i++)i; // 約4.3ms
for(var i=0;i<loop;i++)try{i}catch(e){}; // 約5.8ms
try{for(var i=0;i<loop;i++)i;}catch(e){}; // 約4.9ms
13. 真偽値評価はbooleanで
if文の評価にオブジェクトを書けば null or undefined でfalse扱いとなりますが、booleanで評価したほうが若干高速となるようです。
for(var i=0;i<loop;i++)if(true)i; // 約5.3ms
var o={};
for(var i=0;i<loop;i++)if(o)i; // 約7.5ms
「window.opera」などビルトインオブジェクトをif文の評価に書いてしまいがちです。何度もアクセスする場合であれば、一度boolean型に評価しローカル変数に格納して利用することで高速化が見込めそうです。
var opera = !!window.opera;
if(opera){ ...
まとめ
最近のPCブラウザはスクリプト実行がかなり高速になってるので全てのスクリプトでここまで意識することはないかと思いますが、今回は比較的低スペックなiPhone3Gをターゲットとしたため、細かいスクリプトの書き方による部分を調査してみました。forループの中やmove系のイベントハンドラ、またブラウザのレンダリングをトリガするような処理ではこういった小さい高速化を積み重ねることでレスポンスのよいWebアプリケーションが作れるのではないかと思います。

-
>javascripterさん
コメントありがとうございます。ご指摘の通りです。大変失礼しました。
記事のほうを修正・追記させて頂きました。マピオン中村:2010.6.22 10:32

-
この記事のトラックバックURLhttp://labs.mapion.co.jp/mtos/mt-tb.cgi/67






1. new Object より {}(Objectリテラル)
について、
「for(var i=0;i<loop;i++){}; // 約34ms」
とありますが、forの直後の{}はオブジェクトリテラルではなくブロックとして解釈されるため、オブジェクトの生成が行われません。
new Objectと比較するのであれば、
for(var i=0;i<loop;i++)({})とすべきです。
上記コードで計測し直したところ、差はさらに縮まりました。
javascripter:2010.6.21 19:41