[Из песочницы] Рендеринг WEB-страницы: что об этом должен знать front-end разработчик
Приветствую вас, уважаемые хабравчане! Сегодня я бы хотел осветить вопрос рендеринга в веб-разработке. Конечно, на эту тему уже написано много статей, но, как мне показалась, вся информация довольно разрознена и отрывочна. По крайней мере, чтобы собрать всю картину в своей голове и осмыслить её, мне пришлось проанализировать немало информации (в основном — англоязычной). Именно поэтому я решил формализовать свои знания в статью, и поделиться результатом с сообществом Хабра. Думаю, информация будет полезна как начинающим веб-разработчикам, так и более опытным, чтобы освежить и структурировать свои знания.Данное направление можно и нужно оптимизировать на этапе вёрстки/frontend-разработки, поскольку, очевидно, что разметка, стили и скрипты принимают в рендеринге непосредственное участие. Для этого соответствующие специалисты должны знать некоторые тонкости.Отмечу, что статья нацелена не на точную передачу механики работы браузеров, а, скорее, на понимание её общих принципов. Тем более, разные браузерные движки сильно отличаются в алгоритмах работы, поэтому охватить все нюансы в рамках одной статьи не представляется возможным.
Процесс обработки WEB-страницы браузеромДля начала, рассмотрим последовательность работы браузера при отображении документа: Из полученного от севера HTML-документа формируется DOM (Document Object Model). Загружаются и распознаются стили, формируется CSSOM (CSS Object Model). На основе DOM и CSSOM формируется дерево рендеринга, или render tree — набор объектов рендеринга (Webkit использует термин «renderer», или «render object», а Gecko — «frame»). Render tree дублирует структуру DOM, но сюда не попадают невидимые элементы (например —
, или элементы со стилем 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', '1 px'); // reflow, repaint $body.css ('color', 'red'); // repaint $body.css ('margin', '2 px'); // reflow, repaint // На самом деле произойдет только 1 reflow и repaint Однако, как описано выше, обращение к свойствам элементов вызовет принудительный reflow. То есть, если мы в приведённый блок кода добавим обращение к свойству элемента, это вызовет лишний reflow: var $body = $('body'); $body.css ('padding', '1 px'); $body.css ('padding'); // обращение к свойству, принудительный reflow $body.css ('color', 'red'); $body.css ('margin', '2 px'); В итоге мы получим 2 reflow вместо одного. Поэтому, обращения к свойствам элементов по возможности нужно группировать в одном месте, дабы оптимизировать производительность (см. более подробный пример на JSBin).Но, на практике встречаются ситуации, когда без принудительного reflow не обойтись. Допустим, у нас есть задача: к элементу нужно применить одно и то же свойство (возьмём «margin-left») сначала без анимации (установить в 100 px), а затем — анимировать посредством transition в значение 50 px. Можете сразу посмотреть этот пример на 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); Практические советы по оптимизации На основе данной статьи, а также других статей на Харбе, где освещается вопрос оптимизации JS, можно вывести следующие советы, которые пригодятся при создании эффективного фронтенда: Для более детального изучения вопроса рекомендую ознакомиться со статьями:
Надеюсь, каждый читатель извлёк из статьи что-нибудь полезное. В любом случае — спасибо за внимание!