サーチ…
前書き
JavaScriptは、どの言語と同様に、特定の言語機能を使用する上で賢明であることが求められます。一部の機能を過度に使用するとパフォーマンスが低下する可能性がありますが、一部の手法を使用してパフォーマンスを向上させることができます。
備考
時期尚早の最適化はすべての悪の根源であることに注意してください。明確で正しいコードを書いてから、パフォーマンス上の問題がある場合は、プロファイラを使用して改善すべき特定の領域を探します。意味のある方法で全体的なパフォーマンスに影響を与えない時間最適化コードを無駄にしないでください。
測定、測定、測定。パフォーマンスはしばしば直観に反することがあり、時間とともに変化します。今速いのは、将来的にはうまくいかず、ユースケースに依存する可能性があります。最適化が実際には改善されていることを確認し、パフォーマンスを損なうことなく、変更が価値があることを確認します。
パフォーマンス重視の機能でtry / catchを避ける
一部のJavaScriptエンジン(たとえば、Ignition + turbofanの前の現在のバージョンのNode.jsおよび以前のバージョンのChrome)では、try / catchブロックを含む関数でオプティマイザが実行されません。
パフォーマンスが重要なコードで例外を処理する必要がある場合は、別の機能でtry / catchを保持する方が速い場合があります。たとえば、この関数はいくつかの実装では最適化されません。
function myPerformanceCriticalFunction() {
try {
// do complex calculations here
} catch (e) {
console.log(e);
}
}
しかし、リファクタリングして低速なコードを別の関数(最適化可能 )に移動し、 try
ブロック内から呼び出すことができます。
// This function can be optimized
function doCalculations() {
// do complex calculations here
}
// Still not always optimized, but it's not doing much so the performance doesn't matter
function myPerformanceCriticalFunction() {
try {
doCalculations();
} catch (e) {
console.log(e);
}
}
次の違いを示すjsPerfベンチマークがあります: https ://jsperf.com/try-catch-deoptimization。現在のバージョンのほとんどのブラウザでは、それほど大きな違いはありませんが、ChromeとFirefox、またはIEのバージョンがそれほど新しいものでは、try / catch内のヘルパー関数を呼び出すバージョンが高速になる可能性があります。
このような最適化は、コードのプロファイリングに基づいて慎重に、実際の証拠を用いて行う必要があることに注意してください。 JavaScriptエンジンがうまくいくにつれ、パフォーマンスを損なうことになりかねません。それが助けても、傷ついても、違いを生み出すかは、多くの要因に左右されることがあるので、常にコードへの影響を測定してください。これはすべての最適化に当てはまりますが、特にコンパイラ/ランタイムの低レベルの詳細に依存するこのようなマイクロ最適化は当てはまります。
ヘビーコンピューティング機能にはメモを使用する
プロセッサ(クライアント側またはサーバ側のいずれか)で重い機能を構築する場合は、 以前の関数実行のキャッシュであるmemoizer とその戻り値を考慮する必要があります 。これにより、関数のパラメーターが以前に渡されたかどうかを確認することができます。覚えておいてください、純粋な関数は、入力を与え、対応する一意の出力を返し、スコープの外側に副作用を生じさせない関数なので、予測不可能な関数にmemoizersを追加したり、外部リソース(AJAX呼び出しやランダム戻り値)。
私が再帰的階乗関数を持っているとしましょう:
function fact(num) {
return (num === 0)? 1 : num * fact(num - 1);
}
たとえば、1から100の小さな値を渡すと問題はありませんが、深くなってからは、呼び出しスタックを吹き飛ばしたり、これを行うJavascriptエンジンのプロセスを少し難しくしたり、特に、エンジンがテールコール最適化でカウントしない場合(Douglas CrockfordはネイティブES6にテールコール最適化が含まれていると言っていますが)
私たちは自分の辞書を1から神の知識に固執することができます。それは対応する階乗で何番ですか?しかし、私はそれをアドバイスするかどうか分かりません! memoizerを作りましょうか?
var fact = (function() {
var cache = {}; // Initialise a memory cache object
// Use and return this function to check if val is cached
function checkCache(val) {
if (val in cache) {
console.log('It was in the cache :D');
return cache[val]; // return cached
} else {
cache[val] = factorial(val); // we cache it
return cache[val]; // and then return it
}
/* Other alternatives for checking are:
|| cache.hasOwnProperty(val) or !!cache[val]
|| but wouldn't work if the results of those
|| executions were falsy values.
*/
}
// We create and name the actual function to be used
function factorial(num) {
return (num === 0)? 1 : num * factorial(num - 1);
} // End of factorial function
/* We return the function that checks, not the one
|| that computes because it happens to be recursive,
|| if it weren't you could avoid creating an extra
|| function in this self-invoking closure function.
*/
return checkCache;
}());
これで使用できるようになりました:
今度は私が何をしたかを考え始めると、 numからデクリメントするのではなく1から増やすと、すべての階乗を1からnumまでキャッシュに再帰的にキャッシュすることができましたが、残しておきます。
これは素晴らしいことですが、 複数のパラメータがある場合はどうなりますか?これは問題だ?実際には、arguments配列でJSON.stringify()を使用するか、関数が依存する値のリスト(オブジェクト指向のアプローチの場合)を使用するような素晴らしいテクニックを実行できます。これは、すべての引数と依存関係が含まれる一意の鍵を生成するために行われます。
以前と同じスコープコンセプト(元のものを使用し、キャッシュオブジェクトにアクセスできる新しい関数を返す)を使用して、他の関数を「メモする」関数を作成することもできます。
警告:ES6の構文は、好きではない場合は、...を何も指定せずに置き換え、 var args = Array.prototype.slice.call(null, arguments);
トリック; constを置き換え、varと、あなたがすでに知っている他のものと一緒にしましょう。
function memoize(func) {
let cache = {};
// You can opt for not naming the function
function memoized(...args) {
const argsKey = JSON.stringify(args);
// The same alternatives apply for this example
if (argsKey in cache) {
console.log(argsKey + ' was/were in cache :D');
return cache[argsKey];
} else {
cache[argsKey] = func.apply(null, args); // Cache it
return cache[argsKey]; // And then return it
}
}
return memoized; // Return the memoized function
}
これが複数の引数に対して機能することに気付きましたが、オブジェクト指向の方法ではあまり使用されません。依存関係のために余分なオブジェクトが必要な場合があります。また、 func.apply(null, args)
はfunc(...args)
で置き換えることができますfunc(...args)
なぜなら配列の破壊は配列形式ではなく別々に送るからです。また、参考のために、関数funcへの引数として配列を渡すことは、私が行ったようにFunction.prototype.apply
を使用しない限りうまくいきません。
上記の方法を使用するには、次のようにします。
const newFunction = memoize(oldFunction);
// Assuming new oldFunction just sums/concatenates:
newFunction('meaning of life', 42);
// -> "meaning of life42"
newFunction('meaning of life', 42); // again
// => ["meaning of life",42] was/were in cache :D
// -> "meaning of life42"
コードのベンチマーク - 実行時間の測定
パフォーマンスに関するヒントは、JSエンジンの現在の状態に大きく依存しており、特定の時点でのみ関連すると予想されます。パフォーマンスの最適化の基本的な法則は、最適化を試みる前にまず測定し、推定された最適化の後に再度測定する必要があるということです。
コードの実行時間を測定するには、次のようなさまざまな時間測定ツールを使用できます。
指定されたページのタイミング関連のパフォーマンス情報を表すパフォーマンスインタフェース(ブラウザでのみ使用可能)。
Node.jsのprocess.hrtimeは、[秒、ナノ秒]のタプルとしてタイミング情報を提供します。引数なしで呼び出されると、任意の時刻が返されますが、以前に返された値を引数として呼び出され、2つの実行の差を返します。
コンソールタイマー console.time("labelName")
は、操作の所要時間をトラッキングするために使用できるタイマーを開始します。各タイマーに一意のラベル名を付け、与えられたページで最大10,000個のタイマーを実行することができます。 console.timeEnd("labelName")
を同じ名前で呼び出すと、ブラウザは指定された名前のタイマーを終了し、タイマーの開始後経過した時間をミリ秒単位で出力します。 time()とtimeEnd()に渡される文字列が一致しなければ、タイマーは終了しません。
Date.now関数Date.now()
は、現在のタイムスタンプをミリ秒単位で返します。これは、1970年1月1日00:00:00 UTCから現在までの時間の数値表現です。メソッドnow()はDateの静的メソッドなので、常にDate.now()として使用します。
使用例1 : performance.now()
この例では、関数を実行するための経過時間を計算し、 DOMHighResTimeStamp (ミリ秒単位)を正確に1000分の1ミリ秒に戻すPerformance.now()メソッドを使用します。
let startTime, endTime;
function myFunction() {
//Slow code you want to measure
}
//Get the start time
startTime = performance.now();
//Call the time-consuming function
myFunction();
//Get the end time
endTime = performance.now();
//The difference is how many milliseconds it took to call myFunction()
console.debug('Elapsed time:', (endTime - startTime));
コンソールの結果は次のようになります。
Elapsed time: 0.10000000009313226
performance.now()
使用率は、精度が1000分の1ミリ秒ですが、 互換性が最も低いブラウザで最高の精度を示します。
例2 : Date.now()
を使用する
この例では、大きな配列(100万の値)の初期化の経過時間を計算し、 Date.now()
メソッドを使用します
let t0 = Date.now(); //stores current Timestamp in milliseconds since 1 January 1970 00:00:00 UTC
let arr = []; //store empty array
for (let i = 0; i < 1000000; i++) { //1 million iterations
arr.push(i); //push current i value
}
console.log(Date.now() - t0); //print elapsed time between stored t0 and now
例3 : console.time("label")
& console.timeEnd("label")
この例では、例2と同じタスクを実行していますが、 console.time("label")
& console.timeEnd("label")
メソッドを使用します
console.time("t"); //start new timer for label name: "t"
let arr = []; //store empty array
for(let i = 0; i < 1000000; i++) { //1 million iterations
arr.push(i); //push current i value
}
console.timeEnd("t"); //stop the timer for label name: "t" and print elapsed time
例4では、 process.hrtime()
を使用しprocess.hrtime()
Node.jsプログラムでは、これは費やされた時間を測定する最も正確な方法です。
let start = process.hrtime();
// long execution here, maybe asynchronous
let diff = process.hrtime(start);
// returns for example [ 1, 2325 ]
console.log(`Operation took ${diff[0] * 1e9 + diff[1]} nanoseconds`);
// logs: Operation took 1000002325 nanoseconds
グローバル変数、属性、およびインデックス値に対するローカル変数を優先する
Javascriptエンジンは、最初にローカルスコープ内の変数を検索してから、検索範囲を拡大します。変数が配列内の索引付けされた値または連想配列内の属性である場合、変数を検索する前に親配列を最初に探します。
これは、パフォーマンスに重大なコードを扱う際に意味があります。例えば、共通のfor
ループを取る:
var global_variable = 0;
function foo(){
global_variable = 0;
for (var i=0; i<items.length; i++) {
global_variable += items[i];
}
}
for
ループのすべての反復で、エンジンはitems
を検索し、項目内のlength
属性を検索し、 items
再度検索し、 items
インデックスi
で値を検索し、最後にglobal_variable
検索します。
上記の関数の実行可能な書き換えは次のとおりです。
function foo(){
var local_variable = 0;
for (var i=0, li=items.length; i<li; i++) {
local_variable += items[i];
}
return local_variable;
}
書き換えられたfor
ループのすべての反復で、エンジンはli
を検索し、 items
を検索し、インデックスi
で値を検索し、 local_variable
を検索します。ローカルのスコープを確認するだけです。
オブジェクトを再作成するのではなく再利用する
例A
var i,a,b,len;
a = {x:0,y:0}
function test(){ // return object created each call
return {x:0,y:0};
}
function test1(a){ // return object supplied
a.x=0;
a.y=0;
return a;
}
for(i = 0; i < 100; i ++){ // Loop A
b = test();
}
for(i = 0; i < 100; i ++){ // Loop B
b = test1(a);
}
ループBはループAよりも4倍(400%)高速です
パフォーマンスコードで新しいオブジェクトを作成することは非常に非効率的です。ループAは関数test()
を呼び出します。この関数は呼び出しごとに新しいオブジェクトを返します。作成されたオブジェクトは反復ごとに破棄され、ループBはオブジェクトの戻り値を要求するtest1()
を呼び出しtest1()
。したがって、同じオブジェクトを使用し、新しいオブジェクトの割り当てや過剰なGCヒットを避けます。 (GCは性能試験に含まれなかった)
例B
var i,a,b,len;
a = {x:0,y:0}
function test2(a){
return {x : a.x * 10,y : a.x * 10};
}
function test3(a){
a.x= a.x * 10;
a.y= a.y * 10;
return a;
}
for(i = 0; i < 100; i++){ // Loop A
b = test2({x : 10, y : 10});
}
for(i = 0; i < 100; i++){ // Loop B
a.x = 10;
a.y = 10;
b = test3(a);
}
ループBはループAよりも5倍(500%)高速です
DOMアップデートを制限する
ブラウザ環境でJavaScriptを実行するとよく見られる間違いは、必要以上にDOMを更新することです。
ここで問題となるのは、DOMインターフェイスのすべての更新によってブラウザが画面を再レンダリングすることです。更新によってページ内の要素のレイアウトが変更された場合は、ページレイアウト全体を再計算する必要があり、最も単純な場合でも非常にパフォーマンスが重くなります。ページを再描画するプロセスはリフローと呼ばれ、ブラウザの動作が遅くなったり、応答がなくなったりする可能性があります。
文書をあまり頻繁に更新することの結果は、リストに項目を追加する次の例で説明されています。
<ul>
要素を含む次の文書を考えてみましょう。
<!DOCTYPE html>
<html>
<body>
<ul id="list"></ul>
</body>
</html>
リストに5000
個のアイテムを5000回追加します(これは強力なコンピュータでこれを試して効果を上げることができます)。
var list = document.getElementById("list");
for(var i = 1; i <= 5000; i++) {
list.innerHTML += `<li>item ${i}</li>`; // update 5000 times
}
この場合、1つのDOMアップデートで5000回の変更をすべてバッチすることで、パフォーマンスを向上させることができます。
var list = document.getElementById("list");
var html = "";
for(var i = 1; i <= 5000; i++) {
html += `<li>item ${i}</li>`;
}
list.innerHTML = html; // update once
document.createDocumentFragment()
関数は、ループによって作成されたHTMLの軽量コンテナとして使用できます。このメソッドは、コンテナエレメントのinnerHTML
プロパティを変更する場合(次の図を参照)よりもわずかに高速です。
var list = document.getElementById("list");
var fragment = document.createDocumentFragment();
for(var i = 1; i <= 5000; i++) {
li = document.createElement("li");
li.innerHTML = "item " + i;
fragment.appendChild(li);
i++;
}
list.appendChild(fragment);
オブジェクトプロパティをnullで初期化しています
すべての現代のJavaScript JITコンパイラは、期待されるオブジェクト構造に基づいてコードを最適化しようとしています。 mdnからのヒント
幸運にも、オブジェクトとプロパティはしばしば「予測可能」であり、そのような場合、その基礎構造もまた予測可能である。 JITはこれに基づいて予測可能なアクセスを高速化できます。
オブジェクトを予測可能にする最良の方法は、コンストラクタ内で全体の構造を定義することです。したがって、オブジェクトの作成後にいくつかの追加プロパティを追加する場合は、それらをnull
コンストラクタで定義しnull
。これにより、オプティマイザはライフサイクル全体のオブジェクト動作を予測するのに役立ちます。ただし、すべてのコンパイラには異なるオプティマイザがあり、パフォーマンスの向上は異なる可能性がありますが、値がまだ分かっていなくてもコンストラクタ内のすべてのプロパティを定義することをお勧めします。
いくつかのテストの時間。私のテストでは、forループを使っていくつかのクラスインスタンスの大きな配列を作成しています。ループの中で、配列の初期化の前に、すべてのオブジェクトの "x"プロパティに同じ文字列を割り当てます。コンストラクタが "x"プロパティをnullで初期化すると、配列は余分な文を実行していても常に処理されます。
これはコードです:
function f1() {
var P = function () {
this.value = 1
};
var big_array = new Array(10000000).fill(1).map((x, index)=> {
p = new P();
if (index > 5000000) {
p.x = "some_string";
}
return p;
});
big_array.reduce((sum, p)=> sum + p.value, 0);
}
function f2() {
var P = function () {
this.value = 1;
this.x = null;
};
var big_array = new Array(10000000).fill(1).map((x, index)=> {
p = new P();
if (index > 5000000) {
p.x = "some_string";
}
return p;
});
big_array.reduce((sum, p)=> sum + p.value, 0);
}
(function perform(){
var start = performance.now();
f1();
var duration = performance.now() - start;
console.log('duration of f1 ' + duration);
start = performance.now();
f2();
duration = performance.now() - start;
console.log('duration of f2 ' + duration);
})()
これはChromeとFirefoxの結果です。
FireFox Chrome
--------------------------
f1 6,400 11,400
f2 1,700 9,600
わかりましたが、パフォーマンスの向上は2つの方法で大きく異なります。
一貫してNumbersを使用する
エンジンがあなたの値に特定の小さなタイプを使用していると正しく予測できれば、実行されたコードを最適化することができます。
この例では、配列の要素を合計して時間を出力する簡単な関数を使用します:
// summing properties
var sum = (function(arr){
var start = process.hrtime();
var sum = 0;
for (var i=0; i<arr.length; i++) {
sum += arr[i];
}
var diffSum = process.hrtime(start);
console.log(`Summing took ${diffSum[0] * 1e9 + diffSum[1]} nanoseconds`);
return sum;
})(arr);
配列を作り、要素を合計しましょう:
var N = 12345,
arr = [];
for (var i=0; i<N; i++) arr[i] = Math.random();
結果:
Summing took 384416 nanoseconds
さて、整数だけを使って同じことをやってみましょう:
var N = 12345,
arr = [];
for (var i=0; i<N; i++) arr[i] = Math.round(1000*Math.random());
結果:
Summing took 180520 nanoseconds
合計整数はここで半分になりました。
エンジンは、JavaScriptで使用しているのと同じタイプを使用しません。ご存じのように、JavaScriptの数字はすべてIEEE754の倍精度浮動小数点数です。整数の具体的な表現はありません。しかし、エンジンでは、整数だけを使用すると予測できるときは、よりコンパクトで高速な表現(たとえば、短い整数)を使用できます。
この種の最適化は、計算またはデータ集約型アプリケーションにとって特に重要です。