Давно известно, что операции с 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