Javascript.RU

Исследование скорости переноса ДОМ элементов

Итак задача. Необходимо перенести дочерние элементы из элемента 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}.

+4

Автор: Octane, дата: 6 января, 2010 - 16:03
#permalink

Еще можно попробовать вот такой фокус:

if (document.createRange) {
	$G._test(function () {
		var range = document.createRange();
		range.selectNodeContents(document.getElementById("cont0"));
		document.getElementById("cont1").appendChild(range.extractContents());
	}, "С использованием Range");
}

но в тестовой ситуации, при повторном переносе с помощью Range, работа алгоритма почему-то сильно замедляется (нужно перезагрузить страницу перед тестом):

if (document.createRange) {
	$G._test(function (src, dest) {
		var range = document.createRange();
		range.selectNodeContents(src);
		dest.appendChild(range.extractContents());
	}, "С использованием Range");
}

На современном ноутбуке, с процессором Core i7, выигрыш в оптимизации становится не таким уж большим, по сравнению с увеличением сложности и объема кода:
1676 — 1-й тест в топике
1101 — последний тест в топике

Мои примеры с использованием Range:
162
2691


Автор: tenshi, дата: 7 января, 2010 - 00:02
#permalink

insertBefore может принимать вторым параметром и null

.ня


Автор: bga (не зарегистрирован), дата: 7 января, 2010 - 01:58
#permalink

спасибо. не знал


Автор: PeaceCoder, дата: 7 января, 2010 - 00:54
#permalink

Интересненькая задачка на скорость.


Автор: Илья Кантор, дата: 11 января, 2010 - 21:27
#permalink

С reflow - осторожнее, не факт, что дело в нем.. Браузер делает рендеринг после javascript, если только не нужно что-то измерять.


Автор: subzey, дата: 12 января, 2010 - 11:17
#permalink

Емнимс, Опера может сделать reflow (если нужно, конечно) и до завершения скрипта.
И, кстати, визибилити хидден не спасает от reflow. Лучше все-таки, имхо, оборачивать контент, который предполагается переносить, в див, и переносить его, одной операцией.


Автор: Axdr, дата: 19 января, 2010 - 08:52
#permalink

_copyEls6 в таком виде не работает:
Во-первых, в нижней строке написано _copyEls5, а не _copyEls6
Во-вторых, ошибка в строке

         17   srcP.insertBefore(src,srcN);

из-за того, что после

         09   df2.appendChild(dest);

srcN больше не является дочерним элементом srcP

Метод гениальный. В Google Chrome выполняется за 80 мс


Автор: bga (не зарегистрирован), дата: 21 января, 2010 - 01:31
#permalink

огромное спасибо. Исправил. А так же учел замечает tenshi на практике.


Автор: olimpiada Sochi (не зарегистрирован), дата: 26 января, 2011 - 12:17
#permalink

Браво, какие нужные слова..., блестящая мысль


Автор: monolithed, дата: 6 января, 2013 - 21:51
#permalink

Автор: Гость (не зарегистрирован), дата: 16 апреля, 2022 - 00:19
#permalink

Автор: Гость (не зарегистрирован), дата: 16 апреля, 2022 - 13:27
#permalink

Отправить комментарий

Приветствуются комментарии:
  • Полезные.
  • Дополняющие прочитанное.
  • Вопросы по прочитанному. Именно по прочитанному, чтобы ответ на него помог другим разобраться в предмете статьи. Другие вопросы могут быть удалены.
    Для остальных вопросов и обсуждений есть форум.
P.S. Лучшее "спасибо" - не комментарий, как все здорово, а рекомендация или ссылка на статью.
Содержание этого поля является приватным и не предназначено к показу.
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Разрешены HTML-таги: <strike> <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <u> <i> <b> <pre> <img> <abbr> <blockquote> <h1> <h2> <h3> <h4> <h5> <p> <div> <span> <sub> <sup>
  • Строки и параграфы переносятся автоматически.
  • Текстовые смайлы будут заменены на графические.

Подробнее о форматировании

CAPTCHA
Антиспам
10 + 0 =
Введите результат. Например, для 1+3, введите 4.
 
Поиск по сайту
Содержание

Учебник javascript

Основные элементы языка

Сундучок с инструментами

Интерфейсы

Все об AJAX

Оптимизация

Разное

Дерево всех статей

Популярные таги
Последние комментарии
Последние темы на форуме
Forum