⬆️ ⬇️

JS Library Performance Comparison



Some time ago there was a task to make a comparative analysis of jQuery and Google Closure Library. The main comparison was the functional characteristics, but in addition there was a desire to check the speed of these two libraries. Some knowledge of the internal structure allowed us to make assumptions, but the test results turned out to be somewhat unexpected for me and I decided that it was worth sharing it with the habr-community.



Test organization



Before starting the actual comparison, we had to make a “test engine” - a few lines of code that allowed us to further run several different tests. After that, the comparative testing was also easily added to perform the same operations on “bare” javascript (let's call it native-calls) and using the ExtJS library. It would be possible to add something else, but then the stock of my knowledge was over, and I did not want to study the library just for the sake of the test.

There are no tricks and the most primitive approach to testing. Actually, the measurement was provided by a tiny function that simply performed the required function the necessary number of times and returned the execution speed — the number of operations per millisecond:

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; } 


In order to run several tests of the same type using different libraries, a function was added that takes an entire group of tests as input:

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


This made it possible to make the test call in such a "visual" form:

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


Well, to all this, a function was added, averaging the results of several tests and drawing a beautiful tablet for visual perception. The full code of the test page will be below.



Tested operations



The choice of operations for the test is carried out subjectively - the most frequently used, in my opinion, operations when developing animated web pages. The way of implementing the operation for each of the libraries is also, in my opinion, the most natural - I constantly meet similar fragments in my own and in someone else’s code.



Search item by ID



Probably no web page can do without searching for elements. Everyone knows that the search by id is the most optimal, and use it. The following code was used for the test:

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


')

Search for items by class



Naturally, the case is not limited to search by identifier. Often you have to look for elements in a more “sophisticated” way. For the test, I chose to search by class:

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




Add item



Naturally, one must be able to add elements to the page. For test purposes, adding the same type of span directly to the body was used. Here, the code without using libraries is already significantly longer than with them:

 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); 




Item Class Definition



Naturally, there is often a need to define the properties of the elements. I chose to define the list of classes assigned to the element (the element itself was searched outside the test cycle):

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




Item class change



Usually you don’t even need to define a class - you need to add it, or delete it. All libraries offer a natural toggle method for this case, but on the bare javascript I had to write a whole footwoman:

 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(" "); 




Element style change



Well, the most frequently used operation with an element is setting it with certain css-properties:

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




After putting together all the elements described above, I received a page for testing, the full text of which can be seen under the spoiler. Libraries were used from the corresponding CDNs (version 1.10.2 for jQuery, 4.2.0 for ExtJs and trunk-version for closure). Anyone can save this in an html-file and repeat the test or add something of their own there.

Long 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> 






Test results



I performed the specified test in all browsers installed on my machine. This was done not to compare browsers, but to make sure that the libraries under different browsers behave relatively equally. Accordingly, I did not take care of updating versions either. The results (the number of operations per millisecond) are in the tables below. (The bold type in each cell shows the average value of four tests, the usual values ​​for each of the tests).



Chrome



Version 28.0.1500.72





Opera



Version 12.10.1652





Firefox



Version 22.0





Internet Explorer



Version 9.0.8112.16421





Results



Visually comparative results can be seen in the chart, which is built on the results of testing in Chrome (the results were normalized, so that different groups of tests fit into one diagram). The longer the bar on the graph, the faster:





As expected, the manipulations from DOM to jQuery are relatively slow, but the gap by an order of magnitude came as a surprise to me. But the manipulations with the attributes of elements on both jQuery and Slosure are almost the same (and are noticeably inferior to extJS, which, on the contrary, loses somewhat in manipulations with the DOM). In general, my trust in jQuery after these tests shaken somewhat, but despite this, the auxiliary functions in the test itself were written using this particular library.



I don’t think that far-reaching conclusions should be made from these results - for the vast majority of web applications, it’s not really necessary to perform any of these operations on a massive scale, but sometimes it’s worth paying attention to the tools used and choosing those that are best suited for tasks. None of the libraries prohibits the use of native-methods for working with DOM and, if necessary, you can always refer to them bypassing all library wrappers.

Source: https://habr.com/ru/post/188254/



All Articles