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

в 7:31, , рубрики: extjs, javascript, jquery, Блог компании Mail.Ru Group, Веб-разработка, производительность javascript, метки: , ,

Сравнение производительности 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='https://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
Сравнение производительности JS библиотек

Opera

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

Firefox

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

Internet Explorer

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

Итоги

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

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

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

Автор: bay73

Источник

  1. goso:

    вроде как в runTest надо time/count а не count/time ?

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js