JSライブラリのパフォーマンス比較



少し前に、jQueryとGoogle Closure Libraryの比較分析を行うというタスクが発生しました。 主な機能特性の比較でしたが、これに加えて、これら2つのライブラリの速度を確認したいという要望がありました。 内部構造についてのある程度の知識は仮定を立てることを可能にしましたが、テスト結果は私にとってやや予想外であることが判明し、habrコミュニティと共有する価値があると判断しました。



テスト組織



実際の比較を開始する前に、「テストエンジン」を作成する必要がありました。これは、いくつかの異なるテストを実行できる数行のコードです。 その後、同様の操作をベアJavaScript(ネイティブコールと呼びます)で実行し、ExtJSライブラリを使用することで、比較テストも簡単に補完されました。 他の何かを追加することもできますが、ここで私の知識のストックは終わりました。テストのためだけにライブラリを勉強したくありませんでした。

トリックはなく、テストアプローチが最も原始的です。 実際の測定は、必要な機能を必要な回数だけ実行し、実行速度(ミリ秒あたりの操作数)を返す小さな機能によって提供されました。

runTest = function(test, count){ var start = new Date().getTime(); for(var i=1;i<count;i++) test(); var end = new Date().getTime(); var time = end - start; return count/time; }
      
      





異なるライブラリを使用して同じタイプの複数のテストを実行するために、テスト全体のグループを入力として使用する関数が追加されました。

 runGroup = function(label, tests, count){ var res = {}; for(var key in tests) res[key] = runTest(tests[key], count); saveResult(label, res); }
      
      





これにより、次のような「視覚的な」形式でテストコールを行うことができました。

 runGroup(' ',{ "native": function1, "jQuery": function2, "closure": function3, "extJS": function4 })
      
      





さて、これに加えて、いくつかのテストの結果を平均化する機能が追加され、視覚認識のための美しいプレートが描かれました。 テストページの完全なコードは次のとおりです。



テスト操作



テスト用の操作の選択は主観的に行われました-私の意見では、アニメーションWebページを開発するときに最も頻繁に使用される操作です。 私の意見では、各ライブラリに操作を実装する方法も最も自然な方法です。私は、自分のコードと他の人のコードの両方でこのような断片に常に遭遇しています。



IDでアイテムを検索する



おそらく、要素を検索しないと完全な単一のWebページではありません。 IDによる検索が最も最適であることを誰もが知っており、それを使用します。 次のコードがテストに使用されました。

 document.getElementById('id'); // native goog.dom.getElement('id'); // closure $('#id'); // jQuery Ext.get('id'); // ExtJS
      
      







クラスごとにアイテムを検索する



当然、検索は識別子に限定されません。 多くの場合、より「洗練された」方法で要素を探す必要があります。 テストでは、クラスによる検索を選択しました。

 document.getElementsByClassName('class'); // native goog.dom.getElementByClass('class'); // closure $('.class'); // jQuery Ext.select('.class'); // ExtJS
      
      







アイテムを追加



当然、ページに要素を追加できる必要があります。 テストのために、同じタイプのスパンを直接ボディに追加しました。 ここで、ライブラリを使用しないコードは、ライブラリを使用するよりもすでにかなり長くなっています。

 goog.dom.appendChild(document.body, goog.dom.createDom('span',{class:'testspan'})); // closure $(document.body).append($('<span class="testspan">')); // jQuery Ext.DomHelper.append(document.body, {tag : 'span', cls : 'testspan'}); // ExtJS // native var spn = document.createElement('span'); spn.setAttribute('class','testspan'); document.body.appendChild(spn);
      
      







要素クラスの定義



当然、要素のプロパティを決定する必要性がしばしば生じます。 要素に割り当てられたクラスのリストの定義を選択しました(要素自体はテストサイクル外で検索されました)。

 nElement.getAttribute('class').split(' '); // native goog.dom.classes.get(gElement); // closure jElement.attr('class').split(' '); // jQuery eElement.getAttribute('class').split(' '); // ExtJS
      
      







要素クラスの変更



通常、クラスを定義する必要はありません。追加または削除する必要があります。 すべてのライブラリは、この場合に自然なトグルメソッドを提供しますが、裸のjavascriptでは、混乱を余儀なくされました。

 goog.dom.classes.toggle(gElement, 'testToggle'); // closure jElement.toggleClass('testToggle'); // jQuery var classes = eElement.toggleCls('testToggle'); // ExtJS // native var classes = nElement.className.split(' '); var ind = classes.indexOf('testToggle'); if(ind==-1) classes.push('testToggle'); else classes.splice(ind,1); nElement.className = classes.join(" ");
      
      







アイテムのスタイルを変更する



さて、要素で最も一般的に使用される操作は、特定のCSSプロパティを設定することです。

 nElement.style.backgroundColor = '#aaa'; // native goog.style.setStyle(gElement, {'background-color': '#aaa'}); // closure jElement.css({'background-color': '#aaa'}); // jQuery eElement.setStyle('backgroundColor','#aaa'); // ExtJS
      
      







上記のすべての要素をまとめて、テスト用のページを取得しました。その全文はネタバレの下で見ることができます。 ライブラリは、対応するCDN(jQueryのバージョン1.10.2、ExtJの4.2.0およびクロージャーのトランクバージョン)から使用されました。誰でもこれをhtmlファイルに保存し、テストを繰り返すか、独自の何かを追加できます。

長いHTML
 <!DOCTYPE html> <html> <head> <script src="http://code.jquery.com/jquery-1.10.2.min.js"></script> <script src='http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js'></script> <script src="http://cdn.sencha.com/ext/gpl/4.2.0/ext-all.js"></script> <script> goog.require('goog.dom'); goog.require('goog.dom.classes'); goog.require('goog.style'); </script> <style> table{border-collapse:collapse;} th {font-size:120%; } td {border: solid black 1px; width: 180px; height: 60px; text-align: center; } .rowlabel {width: 120px; text-align: left; background-color: beige;} .avg {font-weight: bold; font-size:120%; color: darkblue;} </style> <title>Benchmark</title> </head> <body> <div id="testid" class="testclass"></div> <button onclick="getBenchmark()">Run</button> <table id="result"></table> </body> </html> <script> var runCount = 4; //       var testSize = 1000; //      // ... getBenchmark = function(){ for(var i = 0;i<runCount;i++) allTests(); showResults(); } allTests = function(){ //        var nElement = document.getElementById('testid'); var gElement = goog.dom.getElement('testid'); var jElement = jQuery('#testid'); var eElement = Ext.get('testid'); //    runGroup('Id lookup',{ "native": function(){var element = document.getElementById('testid');}, "closure": function(){var element = goog.dom.getElement('testid');}, "jQuery": function(){var element = jQuery('#testid');}, "ExtJS": function(){var element = Ext.get('testid');} }, 500*testSize); //    runGroup('Class lookup',{ "native": function(){var elements = document.getElementsByClassName('testclass');}, "closure": function(){var elements = goog.dom.getElementByClass('testclass');}, "jQuery": function(){var elements = jQuery('.testclass');}, "ExtJS": function(){var elements = Ext.select('.testclass');} }, 200*testSize); //   runGroup('Append span',{ "jQuery": function(){jQuery(document.body).append(jQuery('<span class="testspan">'));}, "closure": function(){goog.dom.appendChild(document.body, goog.dom.createDom('span',{class:'testspan'}));}, "ExtJS": function(){Ext.DomHelper.append(document.body, {tag : 'span', cls : 'testspan'});}, "native": function(){ var spn = document.createElement('span'); spn.setAttribute('class','testspan'); document.body.appendChild(spn); } }, testSize); //     jQuery('.testspan').remove(); //    runGroup('Read classes',{ "native": function(){var classes = nElement.getAttribute('class').split(' ');}, "closure": function(){var classes = goog.dom.classes.get(gElement);}, "jQuery": function(){var classes = jElement.attr('class').split(' ');}, "ExtJS": function(){var classes = eElement.getAttribute('class').split(' ');} }, 100*testSize); //    runGroup('Toggle class',{ "closure": function(){goog.dom.classes.toggle(gElement, 'testToggle');}, "jQuery": function(){jElement.toggleClass('testToggle');}, "ExtJS": function(){var classes = eElement.toggleCls('testToggle');}, "native": function(){ var classes = nElement.className.split(' '); var ind = classes.indexOf('testToggle'); if(ind==-1) classes.push('testToggle'); else classes.splice(ind,1); nElement.className = classes.join(" "); } }, 50*testSize); //  css- runGroup('Styling',{ "native": function(){nElement.style.backgroundColor = '#aaa';}, "closure": function(){goog.style.setStyle(gElement, {'background-color': '#aaa'});}, "jQuery": function(){jElement.css({'background-color': '#aaa'});}, "ExtJS": function(){eElement.setStyle('backgroundColor','#aaa');} }, 50*testSize); } var savedResults = {}; var tests = []; //   showResults = function(){ jQuery('#result').empty(); //   -    var str = '<tr><th></th>' for(var i=0;i<tests.length;i++){ str += '<th>' + tests[i] + '</th>'; } str += '</tr>'; for(var label in savedResults){ //      str += '<tr><td class="rowlabel">'+label+'</td>' for(var i=0;i<tests.length;i++){ str += '<td>'; var key = tests[i]; var res = savedResults[label][key]; if(res){ var detail = ''; var total = 0; for(var k=0;k<res.length;k++){ if(k==0) detail += Math.round(res[k]); else detail += ', ' + Math.round(res[k]); total += res[k]; } if(res.length > 0) total = total / res.length; str += '<span class="avg">'+Math.round(total)+'</span><br>'+detail; } str+='</td>'; } } jQuery('#result').append(str); } //   saveResult = function(label, result){ if(!savedResults[label]) savedResults[label] ={}; for(var key in result){ if(tests.indexOf(key)==-1) tests.push(key); if(!savedResults[label][key]) savedResults[label][key] = []; savedResults[label][key].push(result[key]); } } //     runGroup = function(label, tests, count){ var res = {}; for(var key in tests) res[key] = runTest(tests[key], count); saveResult(label, res); } //      runTest = function(test, count){ var start = new Date().getTime(); for(var i=1;i<count;i++) test(); var end = new Date().getTime(); var time = end - start; return count/time; } </script>
      
      









試験結果



マシンにインストールされているすべてのブラウザーで指定されたテストを実行しました。 これはブラウザを比較するためではなく、異なるブラウザの下のライブラリが比較的同じように動作することを確認するために行われました。 したがって、バージョンの更新も気にしませんでした。 以下のタブレットでの結果(ミリ秒あたりの操作数)。 (各セルに太字で示されているのは、4つのテストの平均値、通常の場合は各テストの値です)。



クロム



バージョン28.0.1500.72





オペラ



バージョン12.10.1652





Firefox



バージョン22.0





インターネットエクスプローラー



バージョン9.0.8112.16421





まとめ



比較結果は、Chromeでのテスト結果に基づいて作成されたチャートで明確に見ることができます(テストは異なるチャートのグループが同じチャートに収まるように結果が正規化されています)。 チャート上のバーが長いほど、高速になります。





予想どおり、jQueryでのDOMの操作は比較的遅いものの、桁違いのギャップが私を驚かせました。 ただし、jQueryとClosureの両方の要素の属性を使用した操作はほぼ同じです(そして、反対にDOMの操作で少し失われるextJSに著しく劣っています)。 一般に、これらのテスト後のjQueryへの自信は多少揺れましたが、それにもかかわらず、テスト自体の補助機能はこの特定のライブラリを使用して作成されました。



これらの結果から広範囲にわたる結論を引き出す必要はないと思います-大部分のWebアプリケーションでは、これらの操作の大部分を実行する必要はありませんが、使用するツールに注意を払い、最適なツールを選択する価値がある場合がありますタスク。 DOMを操作するためのネイティブメソッドの使用を禁止しているライブラリはありません。必要に応じて、すべてのライブラリラッパーを介さずにいつでもアクセスできます。



All Articles