Приветствую вас, уважаемые хабравчане! Сегодня я бы хотел осветить вопрос рендеринга в веб-разработке. Конечно, на эту тему уже написано много статей, но, как мне показалась, вся информация довольно разрознена и отрывочна. По крайней мере, чтобы собрать всю картину в своей голове и осмыслить её, мне пришлось проанализировать немало информации (в основном — англоязычной). Именно поэтому я решил формализовать свои знания в статью, и поделиться результатом с сообществом Хабра. Думаю, информация будет полезна как начинающим веб-разработчикам, так и более опытным, чтобы освежить и структурировать свои знания.
Данное направление можно и нужно оптимизировать на этапе вёрстки/frontend-разработки, поскольку, очевидно, что разметка, стили и скрипты принимают в рендеринге непосредственное участие. Для этого соответствующие специалисты должны знать некоторые тонкости.
Отмечу, что статья нацелена не на точную передачу механики работы браузеров, а, скорее, на понимание её общих принципов. Тем более, разные браузерные движки сильно отличаются в алгоритмах работы, поэтому охватить все нюансы в рамках одной статьи не представляется возможным.
Для начала, рассмотрим последовательность работы браузера при отображении документа:
В процессе взаимодействия пользователя со страницей, а также выполнения скриптов, она меняется, что требует повторного выполнения некоторых из вышеперечисленных операций.
В случае изменения стилей элемента, не влияющих на его размеры и положение на странице (например,
Если же изменения затрагивают содержимое, структуру документа, положение элементов — происходит reflow (или relayout). Причинами таких изменений обычно являются:
Браузеры по возможности локализуют repaint и reflow в пределах элементов, подвергнувшимися изменению. Например, изменение размеров абсолютно или фиксировано спозиционированного элемента затронет только сам элемент и его потомков, в то время как изменение статично спозиционированного — повлечет reflow всех элементов, следующих за ним.
Ещё одна особенность — во время выполнения JavaScript браузеры кэшируют вносимые изменения, и применяют их в один проход по завершению работы блока кода. Например, в ходе выполнения данного кода произойдет только один reflow и repaint:
Однако, как описано выше, обращение к свойствам элементов вызовет принудительный reflow. То есть, если мы в приведённый блок кода добавим обращение к свойству элемента, это вызовет лишний reflow:
В итоге мы получим 2 reflow вместо одного. Поэтому, обращения к свойствам элементов по возможности нужно группировать в одном месте, дабы оптимизировать производительность (см. более подробный пример на JSBin).
Но, на практике встречаются ситуации, когда без принудительного reflow не обойтись. Допустим, у нас есть задача: к элементу нужно применить одно и то же свойство (возьмём «
Для начала заведём класс с transition:
Затем, попробуем реализовать задуманное следующим образом:
Данное решение не будет работать, как ожидается, т.к. изменения кэшируются и применяются только в конце блока кода. Нас выручит принудительный reflow, в результате код приобретёт следующий вид, и будет в точности выполнять поставленную задачу:
На основе данной статьи, а также других статей на Харбе, где освещается вопрос оптимизации клиентской части, можно вывести следующие советы, которые пригодятся при создании эффективного фронтенда:
Для более детального изучения вопроса рекомендую ознакомиться со статьями:
Надеюсь, каждый читатель извлёк из статьи что-нибудь полезное. В любом случае — спасибо за внимание!
UPD: Спасибо SelenIT2 и piumosso за верные замечания по поводу эффективности обработки CSS-селекторов.
Данное направление можно и нужно оптимизировать на этапе вёрстки/frontend-разработки, поскольку, очевидно, что разметка, стили и скрипты принимают в рендеринге непосредственное участие. Для этого соответствующие специалисты должны знать некоторые тонкости.
Отмечу, что статья нацелена не на точную передачу механики работы браузеров, а, скорее, на понимание её общих принципов. Тем более, разные браузерные движки сильно отличаются в алгоритмах работы, поэтому охватить все нюансы в рамках одной статьи не представляется возможным.
Процесс обработки WEB-страницы браузером
Для начала, рассмотрим последовательность работы браузера при отображении документа:
- Из полученного от сервера HTML-документа формируется DOM (Document Object Model).
- Загружаются и распознаются стили, формируется CSSOM (CSS Object Model).
- На основе DOM и CSSOM формируется дерево рендеринга, или render tree — набор объектов рендеринга (Webkit использует термин «renderer», или «render object», а Gecko — «frame»). Render tree дублирует структуру DOM, но сюда не попадают невидимые элементы (например —
<head>
, или элементы со стилемdisplay:none;
). Также, каждая строка текста представлена в дереве рендеринга как отдельный renderer. Каждый объект рендеринга содержит соответствующий ему объект DOM (или блок текста), и рассчитанный для этого объекта стиль. Проще говоря, render tree описывает визуальное представление DOM. - Для каждого элемента render tree рассчитывается положение на странице — происходит layout. Браузеры используют поточный метод (flow), при котором в большинстве случаев достаточно одного прохода для размещения всех элементов (для таблиц проходов требуется больше).
- Наконец, происходит отрисовка всего этого добра в браузере — painting.
В процессе взаимодействия пользователя со страницей, а также выполнения скриптов, она меняется, что требует повторного выполнения некоторых из вышеперечисленных операций.
Repaint
В случае изменения стилей элемента, не влияющих на его размеры и положение на странице (например,
background-color
, border-color
, visibility
), браузер просто отрисовывает его заново, с учётом нового стиля — происходит repaint (или restyle).Reflow
Если же изменения затрагивают содержимое, структуру документа, положение элементов — происходит reflow (или relayout). Причинами таких изменений обычно являются:
- Манипуляции с DOM (добавление, удаление, изменение, перестановка элементов);
- Изменение содержимого, в т.ч. текста в полях форм;
- Расчёт или изменение CSS-свойств;
- Добавление, удаление таблиц стилей;
- Манипуляции с атрибутом «
class
»; - Манипуляции с окном браузера — изменения размеров, прокрутка;
- Активация псевдо-классов (например,
:hover
).
Оптимизация со стороны браузера
Браузеры по возможности локализуют repaint и reflow в пределах элементов, подвергнувшимися изменению. Например, изменение размеров абсолютно или фиксировано спозиционированного элемента затронет только сам элемент и его потомков, в то время как изменение статично спозиционированного — повлечет reflow всех элементов, следующих за ним.
Ещё одна особенность — во время выполнения JavaScript браузеры кэшируют вносимые изменения, и применяют их в один проход по завершению работы блока кода. Например, в ходе выполнения данного кода произойдет только один reflow и repaint:
var $body = $('body');
$body.css('padding', '1px'); // reflow, repaint
$body.css('color', 'red'); // repaint
$body.css('margin', '2px'); // reflow, repaint
// На самом деле произойдет только 1 reflow и repaint
Однако, как описано выше, обращение к свойствам элементов вызовет принудительный reflow. То есть, если мы в приведённый блок кода добавим обращение к свойству элемента, это вызовет лишний reflow:
var $body = $('body');
$body.css('padding', '1px');
$body.css('padding'); // обращение к свойству, принудительный reflow
$body.css('color', 'red');
$body.css('margin', '2px');
В итоге мы получим 2 reflow вместо одного. Поэтому, обращения к свойствам элементов по возможности нужно группировать в одном месте, дабы оптимизировать производительность (см. более подробный пример на JSBin).
Но, на практике встречаются ситуации, когда без принудительного reflow не обойтись. Допустим, у нас есть задача: к элементу нужно применить одно и то же свойство (возьмём «
margin-left
») сначала без анимации (установить в 100px
), а затем — анимировать посредством transition в значение 50px
. Можете сразу посмотреть этот пример на JSBin, но я распишу его и тут.Для начала заведём класс с transition:
.has-transition {
-webkit-transition: margin-left 1s ease-out;
-moz-transition: margin-left 1s ease-out;
-o-transition: margin-left 1s ease-out;
transition: margin-left 1s ease-out;
}
Затем, попробуем реализовать задуманное следующим образом:
var $targetElem = $('#targetElemId'); // наш элемент, по умолчанию у него присутствует класс "has-transition"
// убираем класс с transition
$targetElem.removeClass('has-transition');
// меняем свойство, ожидая, что transition отключён, ведь мы убрали класс
$targetElem.css('margin-left', 100);
// ставим класс с transition на место
$targetElem.addClass('has-transition');
// меняем свойство
$targetElem.css('margin-left', 50);
Данное решение не будет работать, как ожидается, т.к. изменения кэшируются и применяются только в конце блока кода. Нас выручит принудительный reflow, в результате код приобретёт следующий вид, и будет в точности выполнять поставленную задачу:
// убираем класс с transition
$(this).removeClass('has-transition');
// меняем свойство
$(this).css('margin-left', 100);
// принудительно вызываем reflow, изменения в классе и свойстве будут применены сразу
$(this)[0].offsetHeight; // как пример, можно использовать любое обращение к свойствам
// ставим класс с transition на место
$(this).addClass('has-transition');
// меняем свойство
$(this).css('margin-left', 50);
Практические советы по оптимизации
На основе данной статьи, а также других статей на Харбе, где освещается вопрос оптимизации клиентской части, можно вывести следующие советы, которые пригодятся при создании эффективного фронтенда:
- Пишите валидный HTML и CSS, с указанием кодировки. Стили лучше включать в
<head>
, а скрипты — в конце<body>
. - Стремитесь упрощать и оптимизировать селекторы CSS (этим часто пренебрегают разработчики, использующие препроцессоры). Чем меньше вложенность — тем лучше. По эффективности обработки селекторы можно расположить в следующем порядке (начиная с наиболее быстрого):
- Идентификатор:
#id
- Класс:
.class
- Тэг:
div
- Соседний селектор:
a + i
- Дочерний селектор:
ul > li
- Универсальный селектор:
*
- Селектор атрибутов:
input[type="text"]
- Всевдоэлементы и псевдоклассы:
a:hover
Следует помнить, что браузер обрабатывает селекторы справа налево, поэтому в качестве ключевого (крайнего правого) селектора лучше использовать наиболее эффективные — идентификатор и класс.
div * {...} // плохо .list li {...} // плохо .list-item {...} // хорошо #list .list-item {...} // хорошо
- Идентификатор:
- В скриптах минимизируйте любую работу с DOM. Кэшируйте всё: свойства, объекты, если подразумевается повторное их использование. При сложных манипуляциях разумно работать с «offline» элементом (т.е. который находится не в DOM, а в памяти), с последующим помещением его в DOM.
- При использовании jQuery для выборки элементов придерживайтесь рекомендаций по составлению селекторов.
- Для изменения стилей элементов лучше модифицировать только атрибут «
class
», и как можно глубже в дереве DOM, это и более грамотно с точки зрения разработки и поддержки (отделение логики от представления), и менее затратно для браузера. - Анимировать желательно только абсолютно и фиксировано спозиционированные элементы.
- Можно отключать сложные :hover анимации во время скроллинга (например, добавляя к body класс «
no-hover
»). Статья на эту тему.
Для более детального изучения вопроса рекомендую ознакомиться со статьями:
Надеюсь, каждый читатель извлёк из статьи что-нибудь полезное. В любом случае — спасибо за внимание!
UPD: Спасибо SelenIT2 и piumosso за верные замечания по поводу эффективности обработки CSS-селекторов.