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