Сравнение производительности JS-библиотек

от автора


Некоторое время назад возникла задача сделать сравнительный анализ jQuery и Google Closure Library. Основным было сравнение функциональных характеристик, но помимо этого появилось желание проверить и скорости работы этих двух библиотек. Некоторые знания о внутреннем устройстве позволяли сделать предположения, но результаты тестов оказались для меня несколько неожиданными и я решил, что стоит поделиться ими с хабра-сообществом.

Организация теста

Перед тем как начать собственно сравнение пришлось сделать «тестовый движок» — несколько строк кода, которые позволяли далее запускать несколько разных тестов. После этого к сравнительному тестированию было легко добавлено также выполнение тех же операций на «голом» javascript (назовем его native-вызовами) и с использованием библиотеки 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-страниц. Способ реализации операции для каждой из библиотек также, по моему мнению, наиболее естественный — я постоянно встречаю подобные фрагменты и в своем и в чужом коде.

Поиск элемента по идентификатору

Без поиска элементов не обходится, наверное, ни одна 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 

Добавление элемента

Естественно, надо уметь добавлять элементы на страницу. Для тестовых целей использовалось добавление однотипных span непосредственно к body. Тут код без использования библиотек уже существенно длиннее, чем с ними:

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 

Изменение класса элемента

Обычно определять класс даже и не нужно — необходимо добавить его, или удалить. Все библиотеки предлагают естественный метод toggle для данного случая, но вот на голом 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 

Собрав воедино все описанные выше элементы я получил страничку для тестирования, полный текст которой можно увидеть под спойлером. Любой желающий может сохранить это в 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> 

Результаты теста

Указанный тест я выполнил во всех браузерах, установленных на моей машине. Это было сделано не с целью сравнить браузеры, а для того, чтобы убедиться, что библиотеки под разными браузерами ведут себя относительно одинаково. Соответственно, заботиться об обновлении версий я тоже не стал. Результаты в табличках ниже. (Жирным шрифтом в каждой ячейке показано усредненное значение четырех тестов, обычным -значения в каждом из тестов).

Chrome

Версия 28.0.1500.72

Opera

Версия 12.10.1652

Firefox

Версия 22.0

Internet Explorer

Версия 9.0.8112.16421

Итоги

Наглядно сравнительные результаты можно увидеть на диаграмме, которая построена по результатами тестирования в Chrome (результаты были нормированы, так чтобы разные группы тестов уместились на одной диаграмме):

Как и ожидалось манипуляции с DOM на jQuery относительно медленные, но разрыв на порядок стал для меня неожиданностью. А вот манипуляции с атрибутами элементов и на jQuery и на Сlosure практически одинаковы (и заметно уступают extJS, который напротив несколько проигрывает в манипуляциях с DOM). В целом мое доверие к jQuery после этих тестов несколько пошатнулось, но, несмотря на это, вспомогательные функции в самом тесте написаны с использованием именно этой библиотеки.

Не думаю, что из этих результатов стоит делать далеко идущие выводы — для подавляющего большинства web-приложений не требуется действительно массового выполнения ни одной из указанных операций, но иногда все-таки стоит обращать внимание на используемые инструменты и выбирать те, которые наилучшим образом подходят для задачи. Ни одна из библиотек не запрещает использование native-методов работы с DOM и при необходимости всегда можно обратиться к ним минуя все библиотечные обертки.

ссылка на оригинал статьи http://habrahabr.ru/company/mailru/blog/188254/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *