Разгоняем jQuery. Часть 1

jQueryСтатей по ускорению jQuery достаточно много, но обычно они не отличаются наглядностью и подробностью, поэтому я решил провести несколько тестов и выделить, те советы по ускорению jQuery, которые действительно работают.

Кеширование

Кеширование результатов выборки заложено в типовую конструкцию применения jQuery.

Цепочка вызовов:

$('#id-of-element').attr('data-value', 'data-for-element')
                   
.css('color', 'red')
                   
.html('<p>html code</p>');

эквивалентна, следующему коду:

var element = $('#id-of-element');
element
.attr('data-value', 'data-for-element');
element
.css('color', 'red');
element
.html('<p>html code</p>');

Уже здесь на одну выборку приходится несколько вызовов методов, и нам остается только расширить эту практику дальше одной цепочки, например, для выноса выборки за пределы цикла:

var el = $('#el-999-9');

for (var i = 0; i < runCount; i++) {
    el
.addClass('el-class')
     
.css('background-color', 'blue')
     
.html(i);
}

или обработчика события:

var el = $('#el-999-9');

$
('#button').click(function(){
    el
.empty()
     
.html('html code');
});

Кеширование эффективно даже на примере самого быстрого селектора #id:

Код тестов:

var runCount = 100;

function testIdLoopNoCache()
{
   
for (var i = 0; i < runCount; i++)
   
{
        $
('#el-999-9').html(i);
   
}
}

function testIdLoopCache()
{
   
var el = $('#el-999-9');

   
for (var i = 0; i < runCount; i++)
   
{
        el
.html(i);
   
}
}

Хороший результат, но давайте проверим селектор посложнее, например, .class1 .class2:

Разница в производительности браузеров настолько велика, что трудно показать их результаты на одном графике, поэтому выделим еще один график для самых быстрых (по крайней мере в этом тесте):

Очевидно, что чем медлительней селектор тем эффективнее кеширование, особенно актуально кеширование выборок в браузерах, не поддерживающих querySelectorAll.

Минимизируйте работу с DOM

Работа с DOM по возможности должна сводиться к минимуму, например, при добавлении нескольких элементов нужно сначала объединить их код, а потом вставить одним вызовом html.

Начнем с тестов:

Код тестов:

function testAppendLi()
{
    $
('#cnt').append('<ol id="list"></ol>');

   
for (var i = 0; i < runCount; i++)
   
{
        $
('#list').append('<li>' + i + '</li>');
   
}
}

function testAppendLiCacheSelection()
{
    $
('#cnt').append('<ol id="list"></ol>');
   
var list = $('#list');

   
for (var i = 0; i < runCount; i++)
   
{
        list
.append('<li>' + i + '</li>');
   
}
}

function testAppendAllLi()
{
    $
('#cnt').append('<ol id="list"></ol>');

   
var list = '';
   
for (var i = 0; i < runCount; i++)
   
{
        list
+= '<li>' + i + '</li>';
   
}
    $
('#list').append(list);
}

function testAppendAllUlLi()
{
   
var list = '';
   
for (var i = 0; i < runCount; i++)
   
{
        list
+= '<li>' + i + '</li>';
   
}
    $
('#cnt').append('<ol id="list">' + list + '</ol>');
}

Тест testAppendLiCacheSelection попал на этот график с той лишь целью, чтобы показать эффект от кеширования (пусть даже очень быстрого селектора #id).

Собирайте в один элемент

Чтобы ускорить вставку большого количества элементов, их нужно обернуть в один элемент.

Обратите внимание, что тест testAppendAllUlLi работает быстрее, чем тест на чистом DOM и практически так же быстро как innerHTML, то же самое касается методов html, after и других методов с таким функционалом.

Значительное ускорение при обертывании кода в один элемент связано со способом которым jQuery внедряет html код в документ. После предварительной обработки кода, создается элемент div и код присваивается его свойству innerHTML, потом поэлементно клонируется в документ, обертывание в кода в один элемент позволяет минимизировать клонирования.

Функция $.each

Не используйте $.each там где важна скорость работы с массивом. $.each  — это вызов функции в контексте объекта и последующая проверка необходимости досрочного выхода из перебора (зависит от возвращаемого функцией значения), очевидно, что простой for без лишних вызвов функций и проверок значительно быстрее $.each. Результаты тестов это подтверждают:

Разница в производительности $.each и for in применительно к объектам не столь значительна, вероятно это связано с тем, что for in не столь быстр как for и дополнительные расходы на вызов функции и if на его фоне не столь заметны.

Не используйте for in для массивов

Ну и еще раз подтверждение известного факта о медлительности цикла for in в применении к массивам:

 

Конкатенация строк

Конкатенация не относится к jQuery, но это одна из часто используемых операций, причем с подводным камнем, она фантастически медленно выполняется в IE6 и IE7.

Если быстрая конкатенация нужна здесь и сейчас, можно использовать Array.push(string) и Array.join(''), но использовать их повсеместно не рекомендуется, так как конкатенация хорошо поддается оптимизации и быстрее join почти во всех современных браузерах (а если не быстрее, значит разработчики использовали не все возможности для ее оптимизации и она может ускориться в будущем).

Тот же самый график без IE6 и IE7:

Код тестов:

function testConcatenationOperator()
{
   
var str = '';
   
   
for (var i = 0; i < count; i++)
   
{
        str
+= words[i];
   
}
   
   
// для оптимизации браузер может не собирать строку
   
// пока она не потребуется в целом виде
   
var forceConcat = str.toString();
}

function testJoin()
{
   
var str = '';
   
var tmpArray = [];
   
   
for (var i = 0; i < count; i++)
   
{
        tmpArray
.push(words[i]);
   
}
   
   
var str = tmpArray.join('');
}

Проблема конкатенации в IE6/7 в том, что она выполняется тривиально, для каждых двух операндов создается буфер в который они последовательно копируются, рассмотрим пример:

var s = 'str1' + 'str12' + 'str123'

В IE6/7 при выполнении первой конкатенации выделяется память под временную строку и в нее копируются строки 'str1' и 'str12', при выполнении второй конкатенации выделяется память под еще одну строку и в нее копируется строки 'str1str12' и 'str123', и т.д. При каждой конкатенации происходит выделение памяти и копирование строк.

В эффективной реализации при выполнении первой конкатенация создается вспомогательный объект, содержащий ссылки на строки 'str1' и 'str12', при выполнении второй конкатенации добавляется ссылка на 'str123' и только когда потребуется значение строки, выделяется память в которую копируются строки 'str1', 'str12', 'str123'.

Подробнее об этом можно почитать в статьях:
Performance issues with "String Concatenation" in JScript1
Insight into String Concatenation in JScript2

Ускорение доступа к переменным

Еще один прием которому можно научиться у jQuery это ускорение глобальных переменных созданием соответствующей локальной.

Метод прост, создаются локальная переменная, равная соответствующей глобальной, а доступ к локальным переменным во многих браузерах быстрее доступа к глобальным переменным, иногда более чем в 10 раз, в худшем случае разницы нет.

Код теста:

var runCount = 1000000;

var globalVariable = 1;

function testGlobalVariable()
{            
   
for (var i = 0; i < runCount; i++)
   
{
        globalVariable
++;
   
}
}

function testLocalVariable()
{
   
var localVariable = 1;

   
for (var i = 0; i < runCount; i++)
   
{
        localVariable
++;
   
}
}

Пример использования оптимизации undefined в приближенных к реальным условиях:

function testArrayLocalUndefined()
{
   
var array = globalArray; //массив в котором каждый 5-й элемент определен
   
var summ = 0;
   
   
var undefined; //ускоритель
   
   
for (var i = 0, l = array.length; i < l; i++) {
       
if (array[i] !== undefined) {
            summ
+= array[i];
       
}
   
}
}

jQuery использует эту технику для ускорения window и undefined.