Примечание: перевод заметки Rendering: repaint, reflow/relayout, restyle от Stoyan Stefanov. Освещается очень много прикладных аспектов отрисовки страниц в браузерах и построения дерева потока документа. Примечания далее курсивом. Термин reflow далее переводится как «перерасчет дерева отрисовка», чтобы отделить его от термина repaint (фактическая перерисовка страницы).
Итак, что происходит в браузере, когда он отрисовывает HTML-страницу на экране? Как он обрабатывает выданную ему кашу из HTML-, CSS- и (возможно) JavaScript-кусков?
Разные браузеры проделывают это по-разному, но следующая диаграмма дает понимание об процессе в целом и более-менее подходит для всех браузеров, как только они загрузили весь код страницы.
documentElement
(тег <html>
). Затраты на время построения DOM-дерева исследовались в этих статьях.-moz
, -webkit
и другие расширения, которые он может не понимать и должен игнорировать. Информация по стилям строится каскадным образом: сначала идут правила по умолчанию из самого браузера (более подробно их размер исследовался в этой статье), затем идут пользовательские стили, затем авторские (автора страницы) — внешние, импортированные, внутренние, а уже потом — все стили, прописанные в атрибутах style
HTML-тегов.div
при помощи display: none
, то он в это дерево не попадет, это не совсем верно, как видно из следующей статьи, но принцип тот). То же самое для других невидимых элементов: например, для тега head
и всех вложенных элементов. С другой стороны в этом дереве DOM-узлы могут быть представлены в виде нескольких узлов, например, если каждая строка <p>
требует своего узла для отрисовки. Узел в таком дереве называется кадр (frame) или блок (box) (так же как и CSS-блок, соответствующий блочной модели). У каждого из этих блоков есть блочные CSS-свойства: ширина, высота, граница, поля, и т.д (таким образом, даже строчные (inline) элементы будут представлены в дереве отрисовки отдельными блоками).Давайте рассмотрим следующий пример.
<html> <head> <title>Beautiful page</title> </head> <body> <p> Когда-то давно-давно здесь был длинный-длинный параграф... </p> <div> Скрытое сообщение </div> <div><img src="..." /></div> ... </body> </html>
DOM-дерево, которое соответствует данному HTML-документу, в основном, содержит по одному узлу на каждый тег и по одному узлу на каждый кусок текста между тегами (для простоты можно игнорировать тот факт, что пробелы тоже являются текстовыми узлами ):
documentElement (html) head title body p [текстовый узел] div [текстовый узел] div img ...
Это дерево отрисовки является видимой частью DOM-дерева. В нем отсутствует ряд вещей — например, заголовок страницы и скрытые элементы — но есть и дополнительные узлы (или кадры, или блоки) для отображения строк текста.
корень (RenderView) body p строка 1 строка 2 строка 3 ... div img ...
Корневой элемент дерева отрисовки является кадром (блоком), содержащим все остальные элементы. Можно считать его внутренней частью окна браузера, так как он ограничивает область, в которой страница может располагаться. Говоря технически языком, в WebKit корневым узлом называется RenderView
, и он соответствует первоначальному контейнеру по спецификации CSS, и является прямоугольной областью видимости, распространяющейся от начала страницы(0
, 0
) до (window.innerWidth
, window.innerHeight
).
Давайте дальше посмотрим, как именно отображение элементов на экране вызывает рекурсивный просмотр дерева перерисовки. Здесь и далее термин (re)flow перевдится как перерасчет дерева перерисовки.
На странице всегда присутствует хотя бы один расчет потока документа вместе с его отрисовкой (если конечно вы не фанат пустой страницы в браузере :)). После этого изменение входной информации, которая была использована для рендеринга страницы, может привести к одному (или обоим) пунктам ниже.
Перерисовки и перерасчеты могут быть весьма затратным мероприятием, отрицательно влиять на ощущения пользователя и замедлять общую реакцию интерфейса на действия пользователя.
Все, что каким-либо образом изменяет информацию, используемую для построения дерева отрисовки (что, естественно, приводит к необходимости пересоздания такого дерева), может привести к перерасчету или перерисовке. Например:
display: none
(перерасчет или перерисовка) или visibility: hidden
(только перерисовка, геометрия не меняется).Давайте рассмотрим пару примеров:
var bstyle = document.body.style; // кэшируем bstyle.padding = "20px"; // перерасчет + перерисовка bstyle.border = "10px solid red"; // еще один перерасчет + перерисовка bstyle.color = "blue"; // только перерисовка, размеры не изменились bstyle.backgroundColor = "#fad"; // перерисовка bstyle.fontSize = "2em"; // перерасчет + перерисовка // новый DOM-элемент = перерасчет + перерисовка document.body.appendChild(document.createTextNode('dude!'));
Некоторые перерасчеты дерева отрисовки могут быть более «тяжелыми». Стоит рассматривать эти операции в терминах дерева отрисовки: если изменения касаются только узла, который является прямым потомком тела документа (body
), то вполне вероятно, что другие узлы не будут затронуты. Но что произойдет, если вы начнете анимировать и увеличить блок вверху страницы, что будет смещать всю остальную страницу вниз? Видимо, это будет весьма затратно.
Поскольку перасчеты потока документы и перерисовки, связанные с изменением дерева отрисовки, являются весьма непростыми операциями, браузеры пытаются всеми силами нивелировать негативные эффекты. Один подход заключается в отказе производить часть работы. По крайней мере, не в данный момент. Браузер может создать некоторую очередь, в которую будет вносить изменения, производимые скриптами, и выполнять эти изменения пачками. В этом случае несколько изменений, каждое из которых повлечет перерасчет, могут быть объединены в одно, и будет произведен только один перерасчет. Браузеры также могут сбрасывать состояние очереди (отрисовывать) страницу по истечению некоторого времени (100-200мс) или достижения некоторого количества (10-20) таких изменений.
Но иногда скрипты могут предотвратить такое оптимизирующее поведение браузеров и заставить применить все накопившиеся в очереди изменения прямо сию секунду. Это происходит, когда вы запрашиваете следующую информацию о стилях:
offsetTop
, offsetLeft
, offsetWidth
, offsetHeight
,scrollTop
/Left/Width/Height,clientTop
/Left/Width/Height,getComputedStyle()
или currentStyle
в IE.Все вышеприведенные варианты запрашивают информацию о стиле DOM-узла у браузера. Этот запрос заставляет браузер выполнить все отложенные операции, чтобы выдать актуальную информацию. Все это приводит к выполнению перерасчета (и видимо, перерисовки).
Например, плохой идеей будет устанавливать стили, запрашивая при этом информацию о них в одном и том же месте (например, внутри цикла):
// нет, только не это! el.style.left = el.offsetLeft + 10 + "px";
Для уменьшении негативных эффектов от перерасчета / перерисовки (относительно пользовательского восприятия) стоит, прежде всего, свести к минимуму любые обращения к стилевой информации, чтобы браузер мог максимально оптимизировать перерасчет дерева отрисовки. Что для этого нужно?
cssText
вместо того, чтобы вносить изменения в свойство style
самого элемента.// плохо var left = 10, top = 10; el.style.left = left + "px"; el.style.top = top + "px"; // лучше el.className += " theclassname"; // или если top и left вычисляются на лету, то лучше... el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
documentFragment
для промежуточных результатов,display: none
(1 перерасчет + перерисовка), затем применяйте все изменения, затем восстанавливайте display
(еще один перерасчте + перерисовка). Тки образом можно сэкономить сотни потенциальных перерасчетов дерева отрисовки.// нет, только не это! for(здесь; длинный; цикл) { el.style.left = el.offsetLeft + 10 + "px"; el.style.top = el.offsetTop + 10 + "px"; } // лучше var left = el.offsetLeft, top = el.offsetTop esty = el.style; for(здесь; длинный; цикл) { left += 10; top += 10; esty.left = left + "px"; esty.top = top + "px"; }
Примерно год назад не было ничего, что обеспечивало бы информацией о перерасчете и перерисовке страницы в браузере (это может быть и не так: я подозреваю, что в MS были свои внутренние средства для разработчиков, но о них никто не знал, ибо они были похоронены в глубине MSDN :P). Теперь ситуация изменилась, и очень сильно.
Во-первых, появилось событие MozAfterPaint
в ночных сборках Firefox, поэтому стали возможны такие плагины, как, например, этот (от Kyle Scholz), который использует это событие. mozAfterPaint — это очень прикольно, но оно сообщает только момент перерисовки (а не перерасчета дерева отрисовки, что является боле затратным).
Далее. DynaTrace Ajax и более поздний SpeedTracer от Google (заметьте: оба трейсят (trace) :)) являются отличными инструментами для отслеживания перерасчетов и перерисовок, первый предназначен для IE, второй — для WebKit.
Однажды в прошлом году Douglas Crockford заметил, что мы, возможно, делаем в CSS очень глупые вещи, но сами о них не знаем. И теперь есть, что возразить на это. Я немного участвовал в проекте на той стадии, что мы разбирались со следующей проблемой: незначительное увеличение пользователем шрифта (in IE6) загружало CPU на 100% в течение 10-15 минут, прежде чем страница снова появлялась на экране.
Отлично, инструменты у нас есть, поэтому для нас уже не может быть поблажек в плане идиотского поведения в CSS.
Ну, за исключением, может быть, одного: было бы, наверное, круто, если бы существовал инструмент, который бы показывал дерево отрисовки в дополнение к DOM-дереву?
Давайте немного подробнее взглянем на указанные инструменты и продемонстрируем с их помощью разницу между изменением стилей (restyle, изменения в дереве отрисовки не касаются геометрии), перерасчетом (reflow, который изменяет макет документа) и перерисовкой (repaint).
Давайте сравним два способа сделать одно и то же. Сначала мы будем менять некоторые стили (не затрагивающие макет) и после каждого изменения будем запрашивать стилевое свойство, никак с изменением не связанное.
bodystyle.color = 'red'; tmp = computed.backgroundColor; bodystyle.color = 'white'; tmp = computed.backgroundImage; bodystyle.color = 'green'; tmp = computed.backgroundAttachment;
А затем то же самое, только все информационные запросы будут осуществляться после всех изменений в стилях:
bodystyle.color = 'yellow'; bodystyle.color = 'pink'; bodystyle.color = 'blue'; tmp = computed.backgroundColor; tmp = computed.backgroundImage; tmp = computed.backgroundAttachment;
В обоих случаях вот определения используемых переменных:
var bodystyle = document.body.style; var computed; if (document.body.currentStyle) { computed = document.body.currentStyle; } else { computed = document.defaultView.getComputedStyle(document.body, ''); }
Теперь пусть эти два примера изменения стилей будут выполняться по клику на документ. Тестовая страница находится здесь: restyle.html (кликаем "dude"). Давайте назовем ее тестом изменения стилей (restyle test).
Во втором тесте будет почти то же самое, только мы будем также менять и информацию о макете:
// каждый раз запрашиваем стили bodystyle.color = 'red'; bodystyle.padding = '1px'; tmp = computed.backgroundColor; bodystyle.color = 'white'; bodystyle.padding = '2px'; tmp = computed.backgroundImage; bodystyle.color = 'green'; bodystyle.padding = '3px'; tmp = computed.backgroundAttachment; // запрашиваем стили только в самом конце bodystyle.color = 'yellow'; bodystyle.padding = '4px'; bodystyle.color = 'pink'; bodystyle.padding = '5px'; bodystyle.color = 'blue'; bodystyle.padding = '6px'; tmp = computed.backgroundColor; tmp = computed.backgroundImage; tmp = computed.backgroundAttachment;
Этот случай давайте назовем тестом изменения макета (relayout test), исходник здесь.
Вот что выдает DynaTrace для первого теста (стили).
Изначально у нас страница загрузилась, затем я кликаю один раз, чтобы выполнить первый сценарий (запрос стилей каждый раз, примерно в 2с). Затем кликаю еще раз, чтобы выполнить второй сценарий (запросы к стилям после всех изменений, примерно в 4с)
Этот инструмент показывает, в какой момент закончилась загрузка страницы (это обозначает логотип IE). Затем курсор мыши находится на области, отвечающей за обработку клика. Увеличиваем масштаб (круто!), и вот более детальная картина:
Тут уже очень хорошо видно, сколько времени ушло на JavaScript (синяя полоска) и сколько — на отрисовку (зеленая полоска). Это у нас еще простой пример, а как отличаются полоски по длине? Наколько дороже для браузера обходится отрисовка, чем исполнение JavaScript? Обычно в Ajax- и Rich-приложениях JavaScript не является узким местом, тормозит доступ к DOM-элементам и часть, отвечающая за отрисовку страницы.
Отлично, теперь запускаем второй тест — на изменение макета, — который меняет геометрию. В этот раз проверяем "PurePaths". Это временная шкала + более детальная информация о каждом элементе на этой шкале. Я подсветил первый клик, где JavaScript вызвал отложенные операции по изменению макета.
Снова увеличиваем масштаб и видим, что дополнительно к полоске «отрисовка» у нас появилась еще одна — «перерасчет дерева отрисовки», потому что в этом тесте мы его сознательно создаем в дополнение к перерисовке.
Теперь давайте протестируем ту же самую страницу в Chrome и посмотрим на результаты SpeedTracer.
Это первый тест по изменению стилей с увеличенным масштабом и обзором происходящего.
Если кратко, то после клика происходит отрисовка. Но в случае первого клика 50% времени уходит на перерасчет стилей. Почему? Потому что мы запрашиваем информацию о стилях (даже не связанную с самими изменяемыми стилями) после каждого изменения.
Увеличиваем события и видим в деталях, что происходит на самом деле: после первого клика стили пересчитываются 3 раза. После второго — только один раз.
Давайте теперь запустим тест на изменение макета. Общая картина выглядит так же, как и в прошлый раз:
Но в случае детального просмотра мы видим, что первый клик вызвал три перерасчета дерева отрисовки (поскольку запрашивал информацию о вычисленных стилях), а второй — только один.
Незначительное отличие между этими инструментами заключается в том, что SpeedTracer не показывает тот момент, когда задача по изменению макета запланирована и добавлена в очередь, а DynaTrace показывает. И поэтому DynaTrace не отображает разницу между изменением стилей и изменением макета, как SpeedTracer. Может быть, просто сам IE не делает между ними разницы? Также DynaTrace не показал три перерасчета дерева отрисовки в первом случае второго теста (когда мы запрашиваем информацию о стилях после каждого изменения макета). Может быть, это так IE работает?
Ниже приведены дополнительные данные касательно этих двух тестов после запуска их достаточное количества раз.
По всем браузерам изменение стилей примерно в два раза быстрее, чем изменение стилей и макета страницы. За исключением IE6, где разницы составляет 4 раза.
Спасибо, что дочитали до этого места. Попробуйте поиграться с инструментами, озвученными выше, и посмотреть за скорость отрисовки страниц. Давайте я еще раз напомню те термины, что использовались в статье.
И немного ссылок на прощание. Стоит отметить, первые три из приведенных материалов объясняют ситуацию более глубоко с точки зрения браузера, а не с точки зрения разработчика (как это попытался сделать я).