Разгони свой сайт

Проект создан как справочный ресурс по методам уменьшения времени загрузки страницы и загрузки сайта у конечного пользователя

English Russian

Проверить скорость загрузки

http://

Статьи Архив статей

Перевод: Мациевский Николай aka sunnybear
Опубликована: 31 декабря 2009

Рендеринг: отрисовка, перерасчет дерева / макета, стилизация

Примечание: перевод заметки Rendering: repaint, reflow/relayout, restyle от Stoyan Stefanov. Освещается очень много прикладных аспектов отрисовки страниц в браузерах и построения дерева потока документа. Примечания далее курсивом. Термин reflow далее переводится как «перерасчет дерева отрисовка», чтобы отделить его от термина repaint (фактическая перерисовка страницы).

Итак, что происходит в браузере, когда он отрисовывает HTML-страницу на экране? Как он обрабатывает выданную ему кашу из HTML-, CSS- и (возможно) JavaScript-кусков?

Процесс отрисовки

Разные браузеры проделывают это по-разному, но следующая диаграмма дает понимание об процессе в целом и более-менее подходит для всех браузеров, как только они загрузили весь код страницы.

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

  • Браузер анализирует HTML-код страницы (кашу из тегов) и строит некоторое DOM-дерево — представление данных, в котором каждому HTML-тегу соответствует свой узел, а текстовые куски между тегами соответствуют своим текстовым узлам. Корневым узлом такого дерева является documentElement (тег <html>). Затраты на время построения DOM-дерева исследовались в этих статьях.
  • Браузер анализирует CSS-код, пытается понять все заложенным в нем хаки и корректно распознать все известные ему приемы, среди которых могут быть -moz, -webkit и другие расширения, которые он может не понимать и должен игнорировать. Информация по стилям строится каскадным образом: сначала идут правила по умолчанию из самого браузера (более подробно их размер исследовался в этой статье), затем идут пользовательские стили, затем авторские (автора страницы) — внешние, импортированные, внутренние, а уже потом — все стили, прописанные в атрибутах style HTML-тегов.
  • После этого наступает самая интересная часть: браузер строит дерево отрисовки. Дерево отрисовки в какой-то мере связано с DOM-деревом, но не соответствует ему полностью. Дерево отрисовки знает о текущих стилях (поэтому если вы спрячете div при помощи display: none, то он в это дерево не попадет, это не совсем верно, как видно из следующей статьи, но принцип тот). То же самое для других невидимых элементов: например, для тега head и всех вложенных элементов. С другой стороны в этом дереве DOM-узлы могут быть представлены в виде нескольких узлов, например, если каждая строка <p> требует своего узла для отрисовки. Узел в таком дереве называется кадр (frame) или блок (box) (так же как и CSS-блок, соответствующий блочной модели). У каждого из этих блоков есть блочные CSS-свойства: ширина, высота, граница, поля, и т.д (таким образом, даже строчные (inline) элементы будут представлены в дереве отрисовки отдельными блоками).
  • После того как готово дерево отрисовки, его можно наконец и отрисовать (paint, draw) на экране.

За деревьями не видно леса

Давайте рассмотрим следующий пример.

Исходный HTML-код:

<html>
<head>
  <title>Beautiful page</title>
</head>
<body>
  <p>
    Когда-то давно-давно здесь был
    длинный-длинный параграф...
  </p>
  <div style="display: none">
    Скрытое сообщение
  </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 перевдится как перерасчет дерева перерисовки.

Перерисовка и перерасчет дерева отрисовки

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

  1. Потребуется пересчитать некоторые части дерева перерисовки (или даже все дерево). Этот процесс называется перерасчет дерева отрисовки (reflow) или перерасчет макета документа (re-layout). Заметьте, что всегда есть хотя 1 такой перерасчет: при первичном отображении страницы.
  2. Некоторые части экрана нужно обновить, либо потому что изменились геометрические свойства узлов, либо какие-либо их стили (например, цвет фона). Этот процесс называется перерисовка (repaint, redraw).

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

Что вызывает перерасчет или перерисовку

Все, что каким-либо образом изменяет информацию, используемую для построения дерева отрисовки (что, естественно, приводит к необходимости пересоздания такого дерева), может привести к перерасчету или перерисовке. Например:

  • Добавление, удаление или обновление DOM-узлов.
  • Скрытие DOM-узла при помощи display: none (перерасчет или перерисовка) или visibility: hidden (только перерисовка, геометрия не меняется).
  • Движение, анимация DOM-элемента на странице.
  • Добавление файла стилей, изменение стилевых свойств элементов.
  • Пользовательские действия, например, изменения размера окна, изменение размера шрифта или (только не это) прокрутка страницы.

Давайте рассмотрим пару примеров:

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) таких изменений.

Но иногда скрипты могут предотвратить такое оптимизирующее поведение браузеров и заставить применить все накопившиеся в очереди изменения прямо сию секунду. Это происходит, когда вы запрашиваете следующую информацию о стилях:

  1. offsetTop, offsetLeft, offsetWidth, offsetHeight,
  2. scrollTop/Left/Width/Height,
  3. clientTop/Left/Width/Height,
  4. 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;";
  • Собирайте изменения DOM-дерева «офлайн» и применяйте их скопом. «Офлайн» в данном случае означает, что изменения происходят не на живом DOM-дереве. Например:
    • используйте 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";
    }
  • Каждый раз перед проведением изменений рассчитывайте, какой эффект они окажут на дерево отрисовки (можно прикидывать по количеству DOM-узлов). Например, использование абсолютного позиционирования превращает элемент в дочерний для тела всего дерев отрисовки, поэтому он не затронет такого количества узлов, например, при анимации. Некоторые элементы (находящиеся глубже по дереву отрисовки) могут быть в области видимости этого элемента, но они потребуют только перерисовки, а не перерасчета дерева.

Инструменты

Примерно год назад не было ничего, что обеспечивало бы информацией о перерасчете и перерисовке страницы в браузере (это может быть и не так: я подозреваю, что в 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 для первого теста (стили).

DynaTrace

Изначально у нас страница загрузилась, затем я кликаю один раз, чтобы выполнить первый сценарий (запрос стилей каждый раз, примерно в 2с). Затем кликаю еще раз, чтобы выполнить второй сценарий (запросы к стилям после всех изменений, примерно в 4с)

Этот инструмент показывает, в какой момент закончилась загрузка страницы (это обозначает логотип IE). Затем курсор мыши находится на области, отвечающей за обработку клика. Увеличиваем масштаб (круто!), и вот более детальная картина:

dynatrace

Тут уже очень хорошо видно, сколько времени ушло на JavaScript (синяя полоска) и сколько — на отрисовку (зеленая полоска). Это у нас еще простой пример, а как отличаются полоски по длине? Наколько дороже для браузера обходится отрисовка, чем исполнение JavaScript? Обычно в Ajax- и Rich-приложениях JavaScript не является узким местом, тормозит доступ к DOM-элементам и часть, отвечающая за отрисовку страницы.

Отлично, теперь запускаем второй тест — на изменение макета, — который меняет геометрию. В этот раз проверяем "PurePaths". Это временная шкала + более детальная информация о каждом элементе на этой шкале. Я подсветил первый клик, где JavaScript вызвал отложенные операции по изменению макета.

dynatrace

Снова увеличиваем масштаб и видим, что дополнительно к полоске «отрисовка» у нас появилась еще одна — «перерасчет дерева отрисовки», потому что в этом тесте мы его сознательно создаем в дополнение к перерисовке.

dynatrace

Теперь давайте протестируем ту же самую страницу в Chrome и посмотрим на результаты SpeedTracer.

Это первый тест по изменению стилей с увеличенным масштабом и обзором происходящего.

speedtracer

Если кратко, то после клика происходит отрисовка. Но в случае первого клика 50% времени уходит на перерасчет стилей. Почему? Потому что мы запрашиваем информацию о стилях (даже не связанную с самими изменяемыми стилями) после каждого изменения.

Увеличиваем события и видим в деталях, что происходит на самом деле: после первого клика стили пересчитываются 3 раза. После второго — только один раз.

speedtracer

Давайте теперь запустим тест на изменение макета. Общая картина выглядит так же, как и в прошлый раз:

speedtracer

Но в случае детального просмотра мы видим, что первый клик вызвал три перерасчета дерева отрисовки (поскольку запрашивал информацию о вычисленных стилях), а второй — только один.

speedtracer

Незначительное отличие между этими инструментами заключается в том, что SpeedTracer не показывает тот момент, когда задача по изменению макета запланирована и добавлена в очередь, а DynaTrace показывает. И поэтому DynaTrace не отображает разницу между изменением стилей и изменением макета, как SpeedTracer. Может быть, просто сам IE не делает между ними разницы? Также DynaTrace не показал три перерасчета дерева отрисовки в первом случае второго теста (когда мы запрашиваем информацию о стилях после каждого изменения макета). Может быть, это так IE работает?

Ниже приведены дополнительные данные касательно этих двух тестов после запуска их достаточное количества раз.

  • В Chrome отказ от запроса вычисленных стилей при их изменении примерно в 2,5 раза быстрее (тест на изменение стилей) и 4,42 раза быстрее, если вы меняете и стили и макет (тест на изменение макеты).
  • В Firefox — в 1,87 и 1,64 раза быстрее, соответственно.

По всем браузерам изменение стилей примерно в два раза быстрее, чем изменение стилей и макета страницы. За исключением IE6, где разницы составляет 4 раза.

Заключение

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

  • Дерево отрисовки — визуальная часть DOM-дерева.
  • Узлы в дереве отрисовки называют кадры или блоки.
  • Изменение дерева отрисовки называется перерасчетом (reflow) в Mozilla и макетом (layout) во всех остальных браузерах, по-видимому.
  • Обновление на экране результатов перерасчета дерева отрисовки называется перерисовкой (repaint, redraw) (в IE/DynaTrace).
  • В SpeedTracer вводится понятие «перерасчета стилей» (стили, не изменяющие геометрию) против «макета».

И немного ссылок на прощание. Стоит отметить, первые три из приведенных материалов объясняют ситуацию более глубоко с точки зрения браузера, а не с точки зрения разработчика (как это попытался сделать я).

Читать дальше

Все комментарии (habrahabr.ru)

Обратная связь

Если у вас возникли какие-либо вопросы или пожелания по работе Web Optimizator, пожалуйста, поделитесь ими, используя контактную информацию.

Спасибо за использование сервиса!

WEBO Site SpeedUp

Книги по оптимизации

Yet Another cSS selector

Ссылки по теме

Техника CSS sprites

Приемы для JavaScript

Хитрости для CSS

Облако сайтов

Вебсайтов сейчас: 1, сегодня: 410, всего: 136915, последний: dressed.ru
© 2008–2010 sunnybear.ru | Карта сайта | RSS (читают 750)

This work is licensed under a Creative Commons Attribution 3.0 Unported License.