Давно известно, что операции с DOM'ом весьма и весьма трудоемки. Потери в производительности заметны обычно в трех случаях:

  • когда скрипт выполняет манипуляции с деревом объектов (создает, удаляет или изменяет часть дерева)
  • если скрипт "заставляет" браузер перерисовывать (redraw) или перестраивать разметку (reflow) страницы
  • и наконец, в случае когда скрипт "ищет" один из узлов дерева объектов (если дерево большое).

Последний пункт я уже рассматривал в одной из статей своего цикла Скаженi кабани, на примере JQuery. Поэтому сегодня мы поговорим о первых двух. Эм...на самом деле я схитрил, и первый пункт представляет собой ни что иное, как причину появления второго пункта. Тогда сразу перейдем ко второму пункту и попробуем разобраться в терминах перерисовывать и перестраивать разметку:

Перерисовка страницы браузером происходит в случае, когда что-то визуально изменилось, но разметка страницы осталась прежней. Например, изменился цвет элемента или элемент стал видимым/невидимым (с помощью visibility: [hidden, visible], так как это не повлияет на разметку). Эта операция существенно влияет на производительность веб приложения, так как заставляет браузер пройтись по дереву объектов и определить какие элементы видимы и как они должны быть отображены.

Перестройка разметки страницы более дорогостоящая операция. Она происходит в следующих случаях:

  • при первой загрузке страницы. В случае с Firefox, перестройка может происходить несколько раз, по мере докачивания контента страницы;
  • когда вы добавляете или удаляете элементы DOM'a. Надо сказать, тут есть одно исключение - если вы добавили/удалили объект с абсолютным позиционированием, то это может и не привести к перестройке разметки страницы, так как позиция и размеры других элементов не были изменены;
  • когда стиль элемента изменен и он влияет на размер и положение этого либо других объектов;
  • когда вы пытаетесь обратиться к свойствам, требующим вычислений со стороны браузера (например, offsetWidth, clientHeight). А также в случае попытки получить вычисляемые CSS значения (с помощью getComputedStyle() или currentStyle в IE).

Процесс перестройки разметки страницы выглядит примерно следующим образом:

Mozilla.org

Wikipedia

Но перестройка разметки странцы не происходит сразу после добавления элемента в дерево или после изменения высоты элемента. Все "запросы" на перестройку разметки выстраиваются в очередь и выполняются уже после выполнения скрипта. Причем, некоторые "запросы" могут быть объединены, если они применяются к одному элементу и являются одного типа (например изменяют его ширину). Но процесс оптимизации таких "запросов" сильно зависит от типа браузера (и даже версии браузера). Пользователь не может взаимодействовать с вашим приложением, пока идет перестройка разметки.

Теперь когда мы в курсе "почему наш сайт тормозит", рассмотрим несколько техник оптимизации работы с DOM'ом. Первое что приходит в голову, это минимизировать количество дорогостоящих операций (описанных выше). А значит, необходимо как можно больше операций совершать вне DOM'а, например используя DocumentFragment.

var products = ... //init array of products
var list = document.getElementById("myProducts"); //find list populate to
for (var i=0; i < products.length; i++){
    var item = document.createElement("li");

item.appendChild(document.createTextNode("Product" + products[i]);
    list.appendChild(item); // AHTUNG! Operation with DOM
}

Предыдущий код можно оптимизировать следующим образом:

var products = ... //init array of products
var list = document.getElementById("myProducts"); //find list populate to
var fragment = document.createDocumentFragment(); //create document fragment
for (var i=0; i < products.length; i++){
    var item = document.createElement("li");
    item.appendChild(document.createTextNode("Product" + products[i]);
    fragment.appendChild(item); //working with fragment only, not with DOM
}
list.appendChild(fragment); //add fragment to DOM

Эта версия кода затрагивает дерево объектов только раз, в последней строчке. И так как DocumentFragment не имеет визуальной составляющей, то все операции с ним не вызывают ни перерисовки, ни изменения в разметке страницы. Но так как DocumentFragment не может быть добавлен в DOM, то операция appendChild добавит все дочерние элементы фрагмента (вместо того, чтобы добавить сам фрагмент).

Другим, более эффективным подходом, будет работать с элементом, не находящимся в дереве. К примеру, мы можем удалить наш объект из дерева перед выполнением операций над ним (removeChild() или replaceChild())

var products = ... //init array of products
var list = document.getElementById("myProducts"); //find list populate to
var parent = list.parentNode; //find parent
parent.removeChild(list); //remove list element from DOM
var fragment = document.createDocumentFragment(); //create document fragment
for (var i=0; i < products.length; i++){
    var item = document.createElement("li");
    item.appendChild(document.createTextNode("Product " + products[i]);
    list.appendChild(item);
}
parent.appendChild(list);

Мы не избежали перестройки разметки страницы, но мы уменьшили количество таких перестроек.

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

element.style.backgroundColor = "white"; //will cause redraw
element.style.color = "red"; //will cause redraw
element.style.fontSize = "12em"; //will cause reflow
element.style.widht = "100px"; //will cause reflow

Как видите первые две строки инициируют перерисовку. Избежать этого можно, если перед изменением стилей скрыть элемент с помощью visibility:hidden или display:none (будет произведено два лишних reflow). Избежать перестройки разметки тут не получиться, но их количество можно уменьшить, если задать все изменения стилей в CSS классе.

.newClass{
background-color: blue;
color: red;
font-size: 12em;
with: 100px;
}

а затем

element.className = "newClass";

Скрытый reflow может также произойти, если мы попытаемся получить одно из вычисляемых свойств (например offsetWidth). Браузер должен быть уверен, что значение свойства актуально, поэтому он рассчитает его заново, а это фактически и будет перестройка разметки страницы. Избежать этого опять же нельзя, но мы можем уменьшить количество таких операций за счет кэширования значения этого свойства:

var posElem = document.getElementById('animation');
var calcWidth = posElem.offsetWidth;
posElem.style.fontSize = ( calcWidth / 10 ) + 'px';
posElem.firstChild.style.marginLeft = ( calcWidth / 20 ) + 'px';
posElem.style.left = ( ( -1 * calcWidth ) / 2 ) + 'px';
... other changes ...

Полезные ссылки

Notes on HTML Reflow

Dom document fragments

Efficent Javascript (from Opera)

Increasing appendChild Performance with DOM Tricks