Posted by Pavel Podlipensky on February 15 4:00 PM
<p>Давно известно, что операции с DOM'ом весьма и весьма трудоемки. Потери в производительности заметны обычно в трех случаях:</p> <ul> <li>когда скрипт выполняет <strong>манипуляции</strong> с деревом объектов (создает, удаляет или изменяет часть дерева) <li>если скрипт "заставляет" браузер <strong>перерисовывать</strong> (redraw) или <strong>перестраивать разметку</strong> (reflow) страницы <li>и наконец, в случае когда скрипт "ищет" один из узлов дерева объектов (если дерево большое).</li></ul> <p>Последний пункт я уже рассматривал в <a href="http://podlipensky.com/post/2008/11/24/quick-web-sites-optimize-jquery.aspx" target="_blank">одной из статей</a> своего цикла <a href="http://podlipensky.com/tag/%D0%BF%D1%80%D0%BE%D0%B8%D0%B7%D0%B2%D0%BE%D0%B4%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D1%8C.aspx" target="_blank">Скаженi кабани</a>, на примере JQuery. Поэтому сегодня мы поговорим о первых двух. Эм...на самом деле я схитрил, и первый пункт представляет собой ни что иное, как причину появления второго пункта. Тогда сразу перейдем ко второму пункту и попробуем разобраться в терминах перерисовывать и перестраивать разметку:</p> <p><strong>Перерисовка страницы </strong>браузером происходит в случае, когда что-то визуально изменилось, но разметка страницы осталась прежней. Например, изменился цвет элемента или элемент стал видимым/невидимым (с помощью visibility: [hidden, visible], так как это не повлияет на разметку). Эта операция существенно влияет на производительность веб приложения, так как заставляет браузер пройтись по дереву объектов и определить какие элементы видимы и как они должны быть отображены.</p> <p>Перестройка разметки страницы более дорогостоящая операция. Она происходит в следующих случаях:</p> <ul> <li>при первой загрузке страницы. В случае с Firefox, перестройка может происходить несколько раз, по мере <strong>докачивания контента</strong> страницы; <li>когда вы добавляете или удаляете элементы DOM'a. Надо сказать, тут есть одно исключение - если вы добавили/удалили объект с абсолютным позиционированием, то это может и не привести к перестройке разметки страницы, так как позиция и размеры других элементов не были изменены; <li>когда стиль элемента изменен и он влияет на размер и положение этого либо других объектов; <li>когда вы пытаетесь обратиться к свойствам, требующим вычислений со стороны браузера (например, offsetWidth, clientHeight). А также в случае попытки получить вычисляемые CSS значения (с помощью getComputedStyle() или currentStyle в IE).</li></ul> <p>Процесс перестройки разметки страницы выглядит примерно следующим образом:</p> <p><a href="http://Mozilla.org" target="_blank" rel="nofollow">Mozilla.org</a></p> <p><span style="display: block"> <object type="application/x-shockwave-flash" height="330" width="400" data="http://video.google.com/googleplayer.swf?docid=1020647662203348823"></object></span></p> <p><a href="http://wiki.org" target="_blank" rel="nofollow">Wikipedia</a></p> <p><span style="display: block"> <object type="application/x-shockwave-flash" height="330" width="400" data="http://video.google.com/googleplayer.swf?docid=-5863446593724321515"></object></span></p> <p>Но перестройка разметки странцы не происходит сразу после добавления элемента в дерево или после изменения высоты элемента. Все "запросы" на перестройку разметки выстраиваются в очередь и выполняются уже после выполнения скрипта. Причем, некоторые "запросы" могут быть объединены, если они применяются к одному элементу и являются одного типа (например изменяют его ширину). Но процесс оптимизации таких "запросов" сильно зависит от типа браузера (и даже версии браузера). Пользователь не может взаимодействовать с вашим приложением, пока идет перестройка разметки.</p> <p>Теперь когда мы в курсе "почему наш сайт тормозит", рассмотрим несколько техник оптимизации работы с DOM'ом. Первое что приходит в голову, это минимизировать количество дорогостоящих операций (описанных выше). А значит, необходимо как можно больше операций совершать вне DOM'а, например используя <a href="http://ejohn.org/blog/dom-documentfragments/" target="_blank" rel="nofollow">DocumentFragment</a>.</p><pre style="font-size: 1.6em"><span style="color: #0000ff">var</span> products = ... <span style="color: #008000">//init array of products</span><br><span style="color: #0000ff">var</span> list = <span style="color: #0000ff">document</span>.getElementById("<span style="color: #8b0000">myProducts</span>"); <span style="color: #008000">//find list populate to</span><br><span style="color: #0000ff">for</span> (<span style="color: #0000ff">var</span> i=0; i < products.<span style="color: #0000ff">length</span>; i++){<br>    <span style="color: #0000ff">var</span> item = <span style="color: #0000ff">document</span>.createElement("<span style="color: #8b0000">li</span>");<p></p> item.appendChild(<span style="color: #0000ff">document</span>.createTextNode("<span style="color: #8b0000">Product</span>" + products[i]);<br>    list.appendChild(item); <span style="color: #008000">// AHTUNG! Operation with DOM</span><br>}</pre><br> <p>Предыдущий код можно оптимизировать следующим образом:</p><pre style="font-size: 1.6em"><span style="color: #0000ff">var</span> products = ... <span style="color: #008000">//init array of products</span><br><span style="color: #0000ff">var</span> list = <span style="color: #0000ff">document</span>.getElementById("<span style="color: #8b0000">myProducts</span>"); <span style="color: #008000">//find list populate to</span><br><span style="color: #0000ff">var</span> fragment = <span style="color: #0000ff">document</span>.createDocumentFragment(); <span style="color: #008000">//create document fragment</span><br><span style="color: #0000ff">for</span> (<span style="color: #0000ff">var</span> i=0; i < products.<span style="color: #0000ff">length</span>; i++){<br>    <span style="color: #0000ff">var</span> item = <span style="color: #0000ff">document</span>.createElement("<span style="color: #8b0000">li</span>");<br>    item.appendChild(<span style="color: #0000ff">document</span>.createTextNode("<span style="color: #8b0000">Product</span>" + products[i]);<br>    fragment.appendChild(item); <span style="color: #008000">//working with fragment only, not with DOM</span><br>}<br>list.appendChild(fragment); <span style="color: #008000">//add fragment to DOM</span></pre><br> <p>Эта версия кода затрагивает дерево объектов только раз, в последней строчке. И так как DocumentFragment не имеет визуальной составляющей, то все операции с ним не вызывают ни перерисовки, ни изменения в разметке страницы. Но так как DocumentFragment не может быть добавлен в DOM, то операция appendChild добавит все дочерние элементы фрагмента (вместо того, чтобы добавить сам фрагмент). </p> <p>Другим, более эффективным подходом, будет работать с элементом, не находящимся в дереве. К примеру, мы можем удалить наш объект из дерева перед выполнением операций над ним (removeChild() или replaceChild())</p><pre style="font-size: 1.6em"><span style="color: #0000ff">var</span> products = ... <span style="color: #008000">//init array of products</span><br><span style="color: #0000ff">var</span> list = <span style="color: #0000ff">document</span>.getElementById("<span style="color: #8b0000">myProducts</span>"); <span style="color: #008000">//find list populate to</span><br><span style="color: #0000ff">var</span> <span style="color: #0000ff">parent</span> = list.parentNode; <span style="color: #008000">//find parent</span><br><span style="color: #0000ff">parent</span>.removeChild(list); <span style="color: #008000">//remove list element from DOM</span><br><span style="color: #0000ff">var</span> fragment = <span style="color: #0000ff">document</span>.createDocumentFragment(); <span style="color: #008000">//create document fragment</span><br><span style="color: #0000ff">for</span> (<span style="color: #0000ff">var</span> i=0; i < products.<span style="color: #0000ff">length</span>; i++){<br>    <span style="color: #0000ff">var</span> item = <span style="color: #0000ff">document</span>.createElement("<span style="color: #8b0000">li</span>");<br>    item.appendChild(<span style="color: #0000ff">document</span>.createTextNode("<span style="color: #8b0000">Product </span>" + products[i]);<br>    list.appendChild(item);<br>}<br><span style="color: #0000ff">parent</span>.appendChild(list);</pre><br> <p>Мы не избежали перестройки разметки страницы, но мы уменьшили количество таких перестроек.</p> <p>Другой причиной перерисовки или перестройки страницы служат <strong>стили</strong> и спобосы их назначения элементам. Рассмотрим следующий кусок кода:</p><pre style="font-size: 1.6em">element.style.backgroundColor = "<span style="color: #8b0000">white</span>"; <span style="color: #008000">//will cause redraw</span><br>element.style.color = "<span style="color: #8b0000">red</span>"; <span style="color: #008000">//will cause redraw</span><br>element.style.fontSize = "<span style="color: #8b0000">12em</span>"; <span style="color: #008000">//will cause reflow</span><br>element.style.widht = "<span style="color: #8b0000">100px</span>"; <span style="color: #008000">//will cause reflow</span></pre><br> <p>Как видите первые две строки инициируют перерисовку. Избежать этого можно, если перед изменением стилей скрыть элемент с помощью visibility:hidden или display:none (будет произведено два лишних reflow). Избежать перестройки разметки тут не получиться, но их количество можно уменьшить, если задать все изменения стилей в CSS классе.</p><pre style="font-size: 1.6em">.<span style="color: #800000">newClass</span>{<br> <span style="color: #800000">background-color</span>: <span style="color: #800000">blue</span>;<br> <span style="color: #800000">color</span>: <span style="color: #800000">red</span>;<br> <span style="color: #800000">font-size</span>: <span style="color: #800000">12em</span>;<br> <span style="color: #800000">with</span>: <span style="color: #800000">100px</span>;<br>}</pre><br> <p>а затем</p><pre style="font-size: 1.6em">element.className = "<span style="color: #8b0000">newClass</span>";</pre><br> <p>Скрытый reflow может также произойти, если мы попытаемся получить одно из вычисляемых свойств (например <strong>offsetWidth</strong>). Браузер должен быть уверен, что значение свойства актуально, поэтому он рассчитает его заново, а это фактически и будет перестройка разметки страницы. Избежать этого опять же нельзя, но мы можем уменьшить количество таких операций за счет кэширования значения этого свойства:</p><pre style="font-size: 1.6em"><span style="color: #0000ff">var</span> posElem = <span style="color: #0000ff">document</span>.getElementById('animation'); <span style="color: #0000ff">var</span> 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 ...</pre> <p><strong>Полезные ссылки</strong></p> <p><a href="http://www.mozilla.org/newlayout/doc/reflow.html" target="_blank" rel="nofollow">Notes on HTML Reflow</a></p> <p><a href="http://ejohn.org/blog/dom-documentfragments/" target="_blank" rel="nofollow">Dom document fragments</a></p> <p><a href="http://dev.opera.com/articles/view/efficient-javascript/?page=3#reflow" target="_blank" rel="nofollow">Efficent Javascript (from Opera)</a></p> <p><a href="http://www.ryboe.com/2008/07/22/increasing-appendchild-performance-with-dom-tricks.html" target="_blank" rel="nofollow">Increasing appendChild Performance with DOM Tricks</a></p>

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

blog comments powered by Disqus