BETA

performanceを使ったJavaScriptのパフォーマンスの測定

投稿日:2019-06-29
最終更新:2019-06-29

はじめに

MDNにて「Performance」という面白そうなものを発見したので使ってみます。

Performance

Performance インターフェイスは、現在のページのパフォーマンス関連情報へのアクセスを提供します。これはHigh Resolution Time API の一部ですが、Performance Timeline API、Navigation Timing API、User Timing API、および Resource Timing API によって拡張されています。
Performance - Web API | MDNより

というわけで、パフォーマンスを測ることができるもののようです。
(new Date()とか使わなくてもいけたんですね…)。

使い方

performance.now()というものを使うと以下のようになります(MDNの例)。

var t0 = performance.now();  
doSomething();  
var t1 = performance.now();  
console.log("Call to doSomething took " + (t1 - t0) + " milliseconds.");  

が、実際にパフォーマンスを測りたい場面ではperformance.markperformance.measureのほうが需要があると思います。

// 処理前を`mark`  
performance.mark('someThing:start');  

// 処理  
doSomething();  

// 処理後を`mark`  
performance.mark('someThing:end');  

// `mark`を関連付ける  
performance.measure('someThing', 'someThing:start', 'someThing:end');  

// 取得して出力  
console.log(performance.getEntriesByType('measure'));  

// clear  
performance.clearMarks();  
performance.clearMeasures();  

処理開始前と処理終了後にperformance.markを使ってマークし、performance.measureでそれを関連付け、performance.getEntriesByType('measure')で取得します。
performance.getEntriesByTypeではPerformanceEntryオブジェクトが時系列順に並んだ返ってきます。

PerformanceEntryオブジェクトには、

  • name
    • 今回は'someThing'
  • entryType
    • 今回は'measure'
  • startTime
    • 処理の開始時間
  • duration
    • 処理でかかった時間
    • 処理の開始時間 - 処理の終了時間

のプロパティがあります。

performance.nowを使う方法と違って、変数に値を代入したり計算したり…といったことをしなくて済むのでとてもありがたいです。

具体例1: 関数合成関数2種類

// データの用意  
const data = JSON.stringify({});  
// 合成する関数の個数  
const loop = 2 ** 10;  

// 再帰を使った関数合成関数  
const recursionComposeFn = (...fns) => (v) => {  
  if (fns.length === 0) return v;  
  return recursionComposeFn(...fns.slice(1))(fns[0](v));  
};  

// Array.prototype.reduceを使った関数合成関数  
const reduceComposeFn = (...fns) => (v) => {  
  return fns.reduce((acc, cur) => {  
    return cur(acc);  
  }, v);  
};  

// 合成する関数ひとつ分  
// 何もしない、ただ重いだけの処理  
const fn = (v) => JSON.stringify(JSON.parse(v));  
// 合成する関数を用意する  
const fns = Array(loop).fill(fn);  

// Ready...  
const recursionCompose = recursionComposeFn(...fns);  
const reduceCompose = reduceComposeFn(...fns);  

// Go!  
performance.mark('recursion:start');  
recursionCompose(data);  
performance.mark('recursion:end');  

// Go!  
performance.mark('reduce:start');  
reduceCompose(data);  
performance.mark('reduce:end');  

// `measure`で関連付け  
performance.measure('recursionCompose', 'recursion:start', 'recursion:end');  
performance.measure('reduceCompose', 'reduce:start', 'reduce:end');  

// 出力  
console.log(performance.getEntriesByType('measure'));  

// clear  
performance.clearMarks();  
performance.clearMeasures();  

各々で確かめてみてください。

私の環境(Google Chrome Canary最新版)での結果としては、reduceを使った方法のほうが10倍以上早く処理が終わりました。というか関数の個数を増やすと再帰する方法ではStack Overflowを起こしてしまいました。

具体例2: 配列のユニーク3種類

// 配列の長さ  
const len = 2 ** 16;  

// UUIDの生成  
// つまり重複はありえない  
const generateUUID = () => {  
    const $ = (a, b) => {  
        return (Math.floor(Math.random() * a) + b).toString(16);  
    };  
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'  
        .replace(/x/g, _ => $(16, 0))  
        .replace(/y/g, _ => $(4, 8));  
};  

// filterとindexOfを使った方法  
const filterIndexOf = (baseArray) => {  
    return baseArray.filter((value, index, array) => {  
        return array.indexOf(value) === index;  
    });  
};  
// reduceとincludeを使った方法  
const reduceInclude = (baseArray) => {  
    return baseArray.reduce((acc, cur) => {  
        if (!acc.includes(cur))  
            acc.push(cur);  
        return acc;  
    }, []);  
};  

// データの用意  
const data = Array(len)  
    .fill(undefined)  
    .map(() => generateUUID());  

// 1  
performance.mark('filterIndexOf:start');  
console.log(filterIndexOf(data));  
performance.mark('filterIndexOf:end');  

// 2  
performance.mark('reduceInclude:start');  
console.log(reduceInclude(data));  
performance.mark('reduceInclude:end');  

// 3  
// Setを使った方法  
performance.mark('Set:start');  
console.log([...new Set(data)]);  
performance.mark('Set:end');  

performance.measure('filterIndexOf', 'filterIndexOf:start', 'filterIndexOf:end');  
performance.measure('reduceInclude', 'reduceInclude:start', 'reduceInclude:end');  
performance.measure('Set', 'Set:start', 'Set:end');  

console.log(performance.getEntriesByType('measure'));  

performance.clearMarks();  
performance.clearMeasures();  

各々で確かめてみてください。

私の結果は、意外なことにSetを使った方法がダントツで早く処理が終わりました(他が1万ミリ秒ほどかかっている中、Setを使った方法は数十ミリ秒ほど)。
Set遅いから使うなと言われていましたが、それは過去の話なのでしょうか?

おわりに

悲しいことに気が付きました。
私の愛するFirefoxは、Google Chromeに対してJavaScriptの実行速度という観点において劣っていることもあるようです。
悲しいです。寝ます。おやすみなさい。

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@okayuの大して技術的ではないブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!