Исследование скорости переноса ДОМ элементов
Итак задача. Необходимо перенести дочерние элементы из элемента src в элемент dest.
Сначала подготовим стенд для экспериментов.
Этот код необходимо запустить чтобы в живую потестить самому.
window.$G=window; // ссылка на глобальную область переменных
$G.$d=document;
// конфигурация стенда
$G.elsCount=3000; // кол-во элементов на контейнер
$G.passCount=20; // кол-во проходов
// функция инициализации контейнера
$G._createEls=function()
{
var df=$d.createDocumentFragment(),n=$G.elsCount;
for(var i=0;i<n;++i)
df.appendChild($d.createElement("div"));
return df;
};
// функция инициализации стенда
$G._main=function()
{
// против многократного запуска
if($d.getElementById("cont0")!=null)
return;
var cont0=$d.createElement("div");
cont0.appendChild($G._createEls());
cont0.id="cont0";
var cont1=$d.createElement("div");
// изначально пустой
cont1.id="cont1";
$d.body.appendChild(cont0);
$d.body.appendChild(cont1);
};
$G._test=function(_func,title)
{
var i,n=$G.passCount/2;
var cont0=$d.getElementById("cont0"),cont1=$d.getElementById("cont1");
// засекаем время
var t0=(new Date()).getTime();
for(i=0;i<n;++i)
{
_func(cont0,cont1);
_func(cont1,cont0);
}
// считаем разницу
t0=(new Date()).getTime()-t0;
// и выводим результат
alert(title+" ,затрачено времени "+t0+" мс.");
};
$G._main();
Теперь присупим к самим тестам.
Результирующее время я привожу для конфигурации Firefox 3.5.6 Intel PentiumM 900 512 озу Win XP.
Простейшая идея это брать первый дочерний элемент src и добавлять в конец dest.При таком алгоритме порядок дочерних элементов не обращается.
$G._copyEls0=function(src,dest)
{
var temp;
while((temp=src.firstChild)!=null)
dest.appendChild(temp);
};
$G._test($G._copyEls0,"Прямой перенос"); // 5200 мс.
Очевидно из за того что браузер не знает цели наших действий и полагает что мы решили просто перенести пару тройку элементов то происходит лавина(reflow) а так же затраты на пересчет стилей от одного до нескольких раз.
Мы можем избежать и того и того временно скрыв как src так и dest. Но не с помощью display="none" который повлияет на разметку и вызовет лавину а с помощью visibility="hidden".
$G._copyEls1=function(src,dest)
{
var temp;
// для упращения не сохраняем предыдущие значения стилей
src.style.visibility=dest.style.visibility="hidden";
while((temp=src.firstChild)!=null)
dest.appendChild(temp);
src.style.visibility=dest.style.visibility="";
};
$G._test($G._copyEls1,"Прямой перенос с сокрытием"); // 5000 мс.
Как видим догадки были верны. Теперь воспользуемся объектом "Фрагмент документа". Он был специально для оптимизации массовой работы с ДОМом. Фрагмент Документа это по сути документ но внутренний и не отображаемый физически. В нем не происходит лавин пересчетов стилей итд. Так же он не имеет HTML головного элемента что позволяет добавлять множество элементов единоразово. Мы попробуем добавить сначала все дочерние элементы src в фрагмент. А потом добавить получившийся фрагмент в dest.
$G._copyEls2=function(src,dest)
{
var temp;
var df=$d.createDocumentFragment();
while((temp=src.firstChild)!=null)
df.appendChild(temp);
dest.appendChild(df);
};
$G._test($G._copyEls2,"Перенос через фрагмент"); // 4650 мс
$G._copyEls3=function(src,dest)
{
var temp;
var df=$d.createDocumentFragment();
src.style.visibility=dest.style.visibility="hidden";
while((temp=src.firstChild)!=null)
df.appendChild(temp);
dest.appendChild(df);
src.style.visibility=dest.style.visibility="";
};
$G._test($G._copyEls3,"Перенос через фрагмент с сокрытием"); // 4550 мс
Теперь проанализируем что же мы все таки сделали. Мы изымали из живого документа по 1 дочернему элементу в фрагмент. Совершенно естественоо что на каждом шаге у браузера происходит частичный пересчет живого документа все равно. Ну как минимум он обновляет firstChild, childNodes и еще массу внутренних значений. Следующей идеей будет локализовать src и dest в фрагменте чтобы сократить кол-во внутренних пересчетов браузера к минимуму. Единственное я бы не рекомендовал делать это с body head или даже html во избежание визуальных артефактов рендера браузера да и скорости мало прибавит. B нельзя забывать восстанавливать как src так и dest в живом документе.
$G._copyEls4=function(src,dest)
{
var temp;
var df=$d.createDocumentFragment();
var df2=$d.createDocumentFragment();
var srcP=src.parentNode,srcN=src.nextSibling; // сохраняем родителя и следующий элемент чтобы восстановить src в живом документе
// переносим src в фрагмент
df2.appendChild(src);
while((temp=src.firstChild)!=null)
df.appendChild(temp);
dest.appendChild(df);
// восстанавливаем из фрагмента
if(srcN==null)
srcP.appendChild(src);
else
srcP.insertBefore(src,srcN);
};
$G._test($G._copyEls4,"Перенос через фрагмент c временным переносом src в фрагмент"); // 3450 мс
$G._copyEls5=function(src,dest)
{
var temp;
var df=$d.createDocumentFragment();
var df2=$d.createDocumentFragment();
var srcP=src.parentNode,srcN=src.nextSibling;
df2.appendChild(src);
dest.style.visibility="hidden";
while((temp=src.firstChild)!=null)
df.appendChild(temp);
dest.appendChild(df);
dest.style.visibility="";
if(srcN==null)
srcP.appendChild(src);
else
srcP.insertBefore(src,srcN);
};
$G._test($G._copyEls5,"Перенос через фрагмент c временным переносом src в фрагмент и сокрытием dest"); // 3600 мс. чуть медленнее обратите внимание.
Далее нет необходимости держать 2 фрагмента так как можно сразу добавлять в локализованный dest.
$G._copyEls6=function(src,dest)
{
var temp;
var df2=$d.createDocumentFragment();
var srcP=src.parentNode,srcN=src.nextSibling;
var destP=dest.parentNode,destN=dest.nextSibling;
df2.appendChild(src);
df2.appendChild(dest);
while((temp=src.firstChild)!=null)
dest.appendChild(temp);
var isInsertSrcFirst= srcN==dest
if(!isInsertSrcFirst)
srcP.insertBefore(src,srcN);
else
destP.insertBefore(dest,destN);
if(isInsertSrcFirst)
srcP.insertBefore(src,srcN);
else
destP.insertBefore(dest,destN);
};
$G._test($G._copyEls6,"Прямой перенос в локализованный dest из локализованного src"); // 2150 мс
Версия с сокрытием не нужна так как и так из живого документа забираем и src и dest.
Собственно по скорости мы достигли апогея с увеличением на 140% что очень и очень. Но это еще не все. Точнее что качается w3c браузеров то все а в IE имеет замечательный метод src.applyElement(dest,"inside") который переносит все дочерние элементы src в dest но при этом dest становится сам дочерним элементом src поэтому нужно перенести обратно dest где был. А так же если в dest изначально были дети то они все переместятся на уровень выше в позицию dest. То есть схематично было body:{a,b,c,dest:{c1,c2,c3},d,e,f} а после var trash=$d.createElement("div"); trash.applyElement(dest,"inside") мы получим body:{a,b,c,c1,c2,c3,d,e,f} и trash:{dest}.
|
Еще можно попробовать вот такой фокус:
но в тестовой ситуации, при повторном переносе с помощью Range, работа алгоритма почему-то сильно замедляется (нужно перезагрузить страницу перед тестом):
На современном ноутбуке, с процессором Core i7, выигрыш в оптимизации становится не таким уж большим, по сравнению с увеличением сложности и объема кода:
1676 — 1-й тест в топике
1101 — последний тест в топике
Мои примеры с использованием Range:
162
2691
insertBefore может принимать вторым параметром и null
.ня
спасибо. не знал
Интересненькая задачка на скорость.
С reflow - осторожнее, не факт, что дело в нем.. Браузер делает рендеринг после javascript, если только не нужно что-то измерять.
Емнимс, Опера может сделать reflow (если нужно, конечно) и до завершения скрипта.
И, кстати, визибилити хидден не спасает от reflow. Лучше все-таки, имхо, оборачивать контент, который предполагается переносить, в див, и переносить его, одной операцией.
_copyEls6 в таком виде не работает:
Во-первых, в нижней строке написано _copyEls5, а не _copyEls6
Во-вторых, ошибка в строке
из-за того, что после
srcN больше не является дочерним элементом srcP
Метод гениальный. В Google Chrome выполняется за 80 мс
огромное спасибо. Исправил. А так же учел замечает tenshi на практике.
Браво, какие нужные слова..., блестящая мысль
Тесты:
http://jsperf.com/children-moving
Angielska roksa
Roksa siechnice
Отправить комментарий
Приветствуются комментарии:Для остальных вопросов и обсуждений есть форум.