Pull to refresh
VK
Building the Internet

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

Reading time8 min
Views38K

Некоторое время назад возникла задача сделать сравнительный анализ 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


Собрав воедино все описанные выше элементы я получил страничку для тестирования, полный текст которой можно увидеть под спойлером. Библиотеки использовались из соответствующих CDN (версия 1.10.2 для jQuery, 4.2.0 для ExtJs и trunk-версия для closure).Любой желающий может сохранить это в 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 и при необходимости всегда можно обратиться к ним минуя все библиотечные обертки.
Tags:
Hubs:
Total votes 101: ↑90 and ↓11+79
Comments92

Articles

Information

Website
vk.com
Registered
Founded
Employees
5,001–10,000 employees
Location
Россия
Representative
Миша Берггрен