Ни в коем случае не reflow!

В CSS-движке браузеров существует несколько операций, затрагивающих изменение картинки на экране браузера. В предыдущих тестах было рассмотрено начальное создание документа и способы ускорения это процесса. Однако все вышеприведенные советы в равной степени относятся и к изменению уже отрисованной картинки при каких-либо операциях с документом.

HTML-элемент в документе может быть скрыт с помощью JavaScript или CSS-свойства display. Дублировать с помощью JavaScript логику, заложенную в CSS-движке, достаточно сложно и не всегда нужно. Проще запросить offsetHeight объекта (если оно равно 0, значит, элемент скрыт). Проще-то оно, конечно, проще, вот только какой ценой?

Для проверки видимости элемента принято проверять значение стиля display или наличие класса hide. Когда мы пишем функцию скрытия/отображения сами, то знаем, какое значение стиля display у объекта по умолчанию или какой класс какому состоянию соответствует. Однако универсальная (библиотечная) функция знать об этом не может.

offsetHeight и style.display

Проведем тестирование скорости вычисления значений offsetHeight и style.display.

Для удобства профайлинга вынесем доступ к этим значениям в отдельные функции:

function fnOffset(el) {
    return !!el.offsetHeight;
}

function fnStyle(el) {
    return el.style.display==’none’;
}

где el – тестовый контейнер.

Проведем тест на тысяче итераций, на каждой итерации будем добавлять в тестовый контейнер элемент <span>. Проверим время, затрачиваемое на добавление тысячи элементов, без вызова тестовых функций тест clean. Проведем тестирование во всех браузерах, замеряя время следующим способом:

var time_start=new Date().getTime();
/* … тест … */
var time_stop=new Date().getTime();
var time_taken=time_stop-time_start;

где time_taken – это время, затраченное на тест, в миллисекундах.

  IE sp62 IE8b Firefox 2.0.0.12 Opera 9.22 Safari 3.04b

clean

128

153

15

15

16

offsetHeight

23500

10624

4453

4453

5140

style.display

171

209

56

56

34

height vs. style

140 раз

50 раз

80 раз

80 раз

150 раз

Таблица 8. Результаты выполнения тестов по определению видимости элементов. Времена приведены в миллисекундах. Взято среднее значение серии из 5 тестов

Судя по результатам тестов, доступ к offsetHeight медленнее в 50-150 раз. Получается, что по отдельности и offsetHeight, и добавление элементов работают быстро, а вместе – очень медленно. Как же так?

Немного теории

Reflow – это процесс рекурсивного обхода ветви дерева DOM, вычисляющий геометрию элементов и их положение относительно родителя. Начало обхода – изменившийся элемент, но возможно и распространение в обратном порядке. Существуют следующие типы reflow:

  • начальный – первичное отображение дерева;
  • инкрементный – возникает при изменениях в DOM;
  • изменение размеров;
  • изменение стилей;
  • «грязный» – объединение нескольких инкрементных reflow, имеющих общего родителя.

Reflow делятся на неотложные (изменение размеров окна или изменение шрифта документа) и асинхронные, которые могут быть отложены и объединены впоследствии.

При манипулировании DOM происходят инкрементные reflow, которые браузер откладывает до конца выполнения скрипта. Однако, исходя из определения reflow, «измерение» элемента вынудит браузер выполнить отложенные reflow. Т.к. возможно распространение снизу вверх, то выполняются все reflow, даже если измеряемый элемент принадлежит к неизменившейся ветви.

Операции reflow очень ресурсоемки и являются одной из причин замедления работы веб- приложений.

Если судить по тесту clean, все браузеры хорошо справляются с кэшированием многочисленных reflow. Однако, запрашивая offsetHeight, мы «измеряем» элемент, что вынуждает браузер выполнить отложенные reflow. Таким образом, браузер делает тысячу reflow в одном случае и только один – в другом.

Замечание: в Opera reflow выполняется еще и по таймеру, что, однако, не мешает ей пройти тест быстрее остальных браузеров. Благодаря этому в Opera виден ход тестов – появляются добавляемые звездочки. Такое поведение оправдано, т.к. вызывает у пользователя ощущение большей скорости браузера.

Использование Computed Style

Что же показало тестирование? По меньшей мере, некорректно сравнивать универсальный (offsetHeight) и частный (style.display) случаи. Тестирование показало, что за универсальность надо платить. Если все-таки хочется универсальности, то можно предложить другой подход: определение Computed Style – конечного стиля элемента (после всех CSS преобразований).

getStyle = function()
{
    var view = document.defaultView;

    if(view && view.getComputedStyle) 
        return function getStyle(el,property) 
        { 
            return view.getComputedStyle(el,null)[property] || 
                el.style[property]; 
            }; 
        return function getStyle(el,property) { 
            return el.currentStyle && el.currentStyle[property] || 
                el.style[property]; 
            };
}();

Проведем тестирование этого способа и сведем все результаты в таблицу. 

  IE sp62 Firefox 2.0.0.12 Opera 9.22 Safari 3.04b

offsetHeight

23500

4453

4453

5140

style.display

171

56

56

34

getStyle     5219 5318

Таблица 9. Резульаты выполнения функции getStyle. Времена приведены в миллисекундах

Во-первых, для IE и Firefox (наиболее популярных браузеров) функция эта работает некорректно (в общем случае возвращает неверные данные). Во-вторых, работает она чуть ли не медленнее, чем offsetHeight.

Вообще говоря, рекомендуется не пользоваться такими универсальными функциями (getStyle есть практически в каждой JavaScript-библиотеке), а реализовывать необходимую функциональность в каждом конкретном случае. Ведь если мы договоримся, что скрытые элементы должны иметь класс hide, то все сведется к определению наличия этого класса у элемента или его родителей.

Оптимизация: определение класса hide

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

function isHidden(el)

    var p=el; var b=document.body; 
    var re=/(^|\s)hide($|\s)/; 
    while(p && p!=b && !re.test(p.className)) 
        p=p.parentNode; 
    return !!p && p!=b;
}

Предполагается, что корневые элементы DOM скрывать не имеет смысла и поэтому проверки ведутся только до document.body.

Предложенное решение явно не спустит лавину reflow, так как никаких вычислений и измерений не проводится. Однако немного смущает проход до корня документа: что же будет при большой вложенности элементов? Давайте проверим. Тест isHidden проводится для вложенности 2 (document.body / test_div), а тест isHidden2 – для вложенности 10 (document.body / div * 8 / test_div).

  IE sp62 Firefox 2.0.0.12 Opera 9.22 Safari 3.04b

offsetHeight

23500

10624

4453

5140

isHidden

231

351

70

71

isHidden2 370 792 212 118

offsetHeight vs. isHidden

102 раза 30 раз 73 раза 92 раза

Таблица 10. Резульаты выполнения функции isHidden. Времена приведены в миллисекундах

Как показывают тесты, даже при большой вложенности падение скорости невелико. Таким образом, мы получили универсальное решение, которое быстрее доступа к offsetHeight в 30-100 раз.

Заключение

Все вышеприведенные мысли предназначены не столько для решения проблемы выяснения видимости элемента в общем случае, сколько для объяснения одного из наиболее часто встречающихся узких мест взаимодействия с DOM и детального разбора методов оптимизации. В ходе тестов был намеренно воспроизведен наихудший случай. В реальных ситуациях такой прирост скорости получится только при использовании в анимации. Однако понимание причин и механизма reflow позволяет писать более оптимальный код.

В качестве послесловия: стили или классы?

В заключении давайте затронем еще несколько оптимизационных моментов, связанных с отображением HTML-страницы на экране браузера. Пусть в нашем документе есть элементы, у которых нужно поменять цвет, фон или что-нибудь еще, относящееся к стилям. Например, подсветить строки таблицы при наведении мыши или пометить их, если выбрана соответствующая галочка в форме. Существует два способа это сделать: при помощи стилей или установив цвет (или фон) напрямую из JavaScript. Для начала немного кода – с помощью класса:

var items = el.getElementsByTagName(‘li’);
    for (var i = 0; i < 1000; i++) { 
    items[i].className = ‘selected’
}

И с помошью стилей:

var items = el.getElementsByTagName(‘li’);
for (var i = 0; i < 1000; i++) { 
    items[i].style.backgroundColor = ‘#007f00’; 
    items[i].style.color = ‘#ff0000’;
}

Результаты простые и понятные:

Метод

IE 6

IE 7

Firefox 1.5

Firefox 2.0

Opera 9

element.className

512

187

291

203

47

element.style.color

1709

422

725

547

282

Таблица 11. Применение стилей и классов к элементам

Перерисовка страницы

Однако когда мы изменяем класс элемента, код отрабатывает значительно быстрее, но вот страница обновляется медленно. Это все из-за того, что изменение свойства className не перерисовывает страницу мгновенно, вместо этого браузер просто помещает событие обновления в очередь reflow. Отсюда и огромная скорость, казалось бы, более сложной процедуры. А что по поводу:hover? К сожалению, :hover работает только для ссылок в Internet Explorer 6. Поэтому в любом случае придется пользоваться какой-то его эмуляцией.

Из всего вышеперечисленного можно сделать два ключевых вывода.

  • Используйте className везде, где это возможно. Это дает больше гибкости и контроля над внешним видом сайта.
  • Если на странице много элементов в контейнере и необходимо построить очень быстрый интерфейс, стоит устанавливать стили напрямую через свойство style.

Групповое изменение стилей

Если мы уже задумались над максимально быстрым изменением интерфейса нашего веб- приложения (отрисовке) через свойство style, то стоит иметь в виду следующий момент. Мы можем изменять свойство cssText, которое отвечает за компилируемые стили элемента:

element.style.cssText = “display:block;width:auto;height:100px;…”;

Таким образом, мы можем дополнительно ускорить наше единовременное обновление стилей у элемента, потому что произойдет всего одно присвоение свойств и всего один reflow (и он случится сразу же после изменения этого свойства, а не в отложенном режиме).

Два слова о таблицах

Таблицы замечательно подходят для организации информации. Однако если в HTML- документе встречается таблица, то браузеру приходится пробежаться по ней дважды: в первый раз – чтобы выбрать все элементы, рассчитать их взаимные размеры, и чтобы отрисовать их все – во второй раз. Если на странице выводятся большие массивы данных (например, параметры товаров или статистические данные), то гораздо быстрее будет визуализировать такие таблицы в один проход.

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

  • Необходимо установить для table CSS-атрибут table-layout в значение fixed.
  • Затем явно определить объекты col для каждого столбца.
  • И установить для каждого элемента col атрибут width.

В качестве примера можно привести такой фрагмент кода:

<table style=.table-layout: fixed.>
<!– первый столбец имеет ширину 100 пикселей –>
<col width=.100.></col>
<!– второй – 200 –>
<col width=.200.></col>
<!– третий и четвертый – по 250 –>
<col width=.250.></col><col width=.250.></col>
<thead>…</thead>
<tfoot>…</tfoot>
<tbody>…</tbody>
</table>

Материалы близкой тематики:

Posted in Разгони свой сайт.