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