Как браузер отрисовывает страницу
Данное направление можно и нужно оптимизировать на этапе вёрстки/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:
Однако, как описано выше, обращение к свойствам элементов вызовет принудительный reflow. То есть, если мы в приведённый блок кода добавим обращение к свойству элемента, это вызовет лишний reflow:
В итоге мы получим 2 reflow вместо одного. Поэтому, обращения к свойствам элементов по возможности нужно группировать в одном месте, дабы оптимизировать производительность (см. более подробный пример на JSBin).
Но, на практике встречаются ситуации, когда без принудительного reflow не обойтись. Допустим, у нас есть задача: к элементу нужно применить одно и то же свойство (возьмём « margin-left ») сначала без анимации (установить в 100px ), а затем — анимировать посредством transition в значение 50px . Можете сразу посмотреть этот пример на JSBin, но я распишу его и тут.
Для начала заведём класс с transition:
Затем, попробуем реализовать задуманное следующим образом:
Данное решение не будет работать, как ожидается, т.к. изменения кэшируются и применяются только в конце блока кода. Нас выручит принудительный reflow, в результате код приобретёт следующий вид, и будет в точности выполнять поставленную задачу:
Практические советы по оптимизации
На основе данной статьи, а также других статей на Харбе, где освещается вопрос оптимизации клиентской части, можно вывести следующие советы, которые пригодятся при создании эффективного фронтенда:
Надеюсь, каждый читатель извлёк из статьи что-нибудь полезное. В любом случае — спасибо за внимание!
UPD: Спасибо SelenIT2 и piumosso за верные замечания по поводу эффективности обработки CSS-селекторов.
Вы вводите название сайта в адресную строку браузера, нажимаете enter, и по привычке видите запрашиваемую страницу. Все просто: ввел название сайта — сайт отобразился. Однако для более любознательных хочу рассказать, что происходит между тем как браузер начинает получать куски сайта (да, сайт приходит кусками, по-другому — чанками) и отображает полностью нарисованную страницу.
Как устроен браузер?
Перед историей о том, как браузер рисует страницу, важно понять как он устроен, какие процессы и на каком уровне выполняются. При знакомстве с процессом рендеринга мы не раз вспомним о компонентах браузера. Итак, под капотом браузер выглядит примерно следующим образом:
User Interface — это все что видит пользователь: адресная строка, кнопки вперед/назад, меню, закладки — за исключением области где отображается сайт.
Browser Engine отвечает за взаимодействие между User Interface и Rendering Engine. Например клик по кнопке назад должен сказать компоненте RE что нужно отрисовать предыдущее состояние.
Rendering Engine отвечает за отображение веб-страницы. В зависимости от типа файла, эта компонента может парсить и рендерить как HTML/XML и CSS, так и PDF .
Network выполняет xhr запросы за ресурсами, и в целом, общение браузера с остальным интернетом происходит через эту компоненту, включая проксирование, кэширование и так далее.
JS Engine место где парсится и исполняется js код.
UI Backend используется чтобы рисовать стандартные компоненты типа чекбоксов, инпутов, кнопок.
Data Persistence отвечает за хранение локальных данных, например в куках, SessionStorage, indexDB и так далее.
Далее узнаем как рассмотренные компоненты браузера взаимодействуют между собой и разберем подробнее, что происходит внутри Rendering Engine. Другими словами …
Как браузер переводит html в пиксели на экране?
Итак, с помощью компонента Network браузер начал получать html-файл чанками обычно по 8кб, что дальше? А далее идет процесс парсинга (спецификация процесса) и рендеринга этого файла в компоненте, как вы уже догадались — Rendering Engine.
Важно! Для повышения юзабилити, браузер не дожидается пока загрузится и распарсится весь html. Вместо этого браузер сразу пытается отобразить пользователю страницу (далее рассмотрим как).
Сам процесс парсинга выглядит так:
Результатом парсинга является DOM дерево. Возьмем к примеру такой html:
DOM дерево такого html файла будет выглядеть так:
По мере того как браузер парсит html файл, он встречает теги содержащие ссылки на сторонние ресурсы ( , , и так далее) — по мере их обнаружения происходит запрос за этими ресурсами.
Таким образом, отправив запрос по адресу прописанному в атрибуте href тега и получив файл css стилей, браузер парсит этот файл и строит так называемый CSS Object Model — CSSOM.
Представим что у нас есть такой файл стилей:
Из которого получим такой CSSOM:
Attention: тут построено дерево из стилей нашего css-файла. Кроме того, также есть user agent's styles — дефолтные стили браузера и инлайновые стили — прописанные в html тегах.
Подробнее об алгоритме парсинга css стилей можно прочитать в спецификации.
Теперь у нас есть DOM и CSSOM - первый отвечает на вопрос «что?», второй на вопрос «как?». Если думаете, что следующим этапом является соединение DOM и CSSOM'а, то вы совершенно правы! DOM + CSSOM = Render Tree.
Render Tree — это дерево видимых (!) элементов построенных в том порядке, в котором они должны рендериться на странице. Обратите внимание, что элементы имеющие css правило display: none или другие, отрицательно влияющие на отображение — не будут находится в render tree.
Браузер строит Render Tree чтобы точно определить что ему нужно отрисовать и в каком порядке. Построение Render дерева происходит примерно так: начиная с рутового элемента (html), парсер проходит по всем видимым элементам (пропуская link, script, meta, скрытые через css элементы) и для каждого видимого элемента находит соответствующее css правило из CSSOM.
В движке firefox'a элементы Render Tree называются фреймами (frames). Webkit использует термин renderer или render object. Render object знает как разместить себя на странице, а так же содержит информацию о своих дочерних элементах. И для самых любознательных, если заглянуть в исходники webkit'a — можно найти класс который так и называется — RenderObject.
Продолжая наш пример мы получим такой Render Tree:
На данный момент мы имеем в некотором состоянии Render Tree — дерево содержащее информацию о том что и как нужно отрисовать. Теперь браузер должен понять на каком месте и с какими размерами будет отображаться элемент. Процесс вычисления позиции и размеров называется Layout.
Layout — это рекурсивный процесс определения положения и размеров элементов из Render Tree. Он начинается от корневого Render Object, которым является , и проходит рекурсивно вниз по части или всей иерархии дерева высчитывая геометрические размеры дочерних render object'ов. Корневой элемент имеет позицию (0,0) и его размеры равны размерам видимой части окна, то есть размеру viewport'a.
В Html используется поточная модель компоновки (flow based layout), другими словами геометрические размеры элементов в некоторых случаях можно рассчитать за один проход (если элементы, встречающиеся в потоке позже, не влияют на позицию и размеры уже пройденных элементов).
Layout может быть глобальный, когда требуется рассчитать положение render object'ов всего дерева, и инкрементальный, когда требуется рассчитать только часть дерева. Глобальный layout происходит, например, при изменении размеров шрифта или при событии resize'a. Инкрементальный layout происходит только для render object'ов, помеченных как «dirty».
Пара слов о «системе грязных битов (dirty bit system)». Эта система используется браузерами для оптимизации процесса, чтобы не пересчитывать весь layout. При добавлении нового или изменении существующего render object — он сам и его дочерние элементы помечаются флагом «dirty». Если render object не изменяется, но его дочерние элементы были изменены или добавлены, то этот render object помечается как «children are dirty».
К концу процесса layout каждый render object имеет свое положение и размеры.
Подводя промежуточный итог: браузер знает что, как и где рисовать. Следовательно — осталось только нарисовать. Этот процесс, как ни странно, называется Paint.
Paint — этап, где пиксель монитора заполняется цветом указанным в свойствах render object'а и белый экран превращается в картину задуманную автором (разработчиком). На всем пути рендеринга — это самый дорогой процесс (не то чтобы предыдущее дешевые).
Также, как и процесс layout, отрисовка (paint) может быть глобальной — дерево перерисовывается полностью, и инкрементальной — дерево перерисовывается частично. Для частичного перерисовывания render object помечает свой rectangle как невалидный. Операционная система расценивает эту область как требующую перерисовки и вызывает событие paint. При этом браузер умеет объединять области, чтобы выполнить разом перерисовку для всех мест, где это необходимо.
Определение размеров и положения элементов дерева (layout) и перерисовка (paint) являются дорогостоящими процессами. Они выполняются на уровне CPU. Разрабатывая динамические веб приложения, в которых эти процессы будут запускаться очень часто — мы никогда не достигнем плавных анимаций.
Значит, должно быть что-то, что помогло бы создавать сайты с богатой анимацией, при этом не нагружая CPU и рисуя каждый кадр менее чем за 16,6мс (60 fps). Действительно, браузер выполняет еще один этап, который помогает оптимизировать динамику сайтов — Composite (композиция).
Перед композицией, все нарисованные элементы находятся на одном слое (memory layer). То есть, изменение параметров (например, геометрических размеров или положения) одних элементов повлекут перерасчет параметров соседних элементов. Но если распределить элементы на композиционные слои — изменение параметров элемента вызовут перерасчет только на определенном слое, не затрагивая при этом элементы на других слоях. Таким образом, этот процесс является самым дешевым по производительности, поэтому нужно стараться вносить изменения вызывающие только composite.
Резюмируя вышесказанное, получаем такой процесс рендеринга веб страницы:
TLDR;
Браузер получает html файл, парсит его и строит DOM. Встречая css стили, браузер их подгружает, парсит, строит CSSOM и объединяет вместе с DOM'ом — получаем Render Tree. Осталось выяснить где расположить элементы из Render Tree — этим занимается задача layout. После расположения элементов, можно начать рисовать их — это задача paint, этап на котором заполняются пиксели экрана.
Динамика
При добавлении новой ноды в dom дерево — очевидно браузеру нужно добавить новый объект в дерево, посчитать его положение на странице, посчитать положения других элементов на странице (если они были аффектнуты новым элементом), и в конце все это нарисовать — звучит дорого. Поэтому делая такие операции необходимо иметь в виду производительность, ведь не каждый пользователь интернета запускает ваше веб-приложение на самой последней модели устройства.
Подводя итог, мы рассмотрели из каких компонентов состоит браузер, как они взаимодействуют друг с другом и как Rendering Engine рисует страницу пользователю.
Посмотреть вышеописанное можно в devtools'ах хрома, но чтобы не выходить за рамки названия статьи — на этом пока все.
Сегодня, в переводе одиннадцатой части серии материалов, посвящённых JavaScript, мы поговорим о подсистемах браузера, ответственных за рендеринг веб-страниц. Они играют ключевую роль в деле преобразования описаний документов, выполненных с помощью HTML и CSS, в то, что мы видим на экране.
Автор материала говорит, что в компании SessionStack приходится уделять рендерингу огромное внимание. В этой статье он поделится советами, касающимися оптимизации веб-страниц с учётом особенностей их визуализации.
Обзор
Создавая веб-приложения, мы не пишем изолированный JS-код, который занимается исключительно какими-то собственными «внутренними» делами. Этот код выполняется в окружении, предоставляемом ему браузером, взаимодействует с ним. Понимание устройства этого окружения, того, как оно работает, из каких частей состоит, позволяет разработчику создавать более качественные программы, даёт ему возможность предусмотреть возникновение возможных проблем с приложением, которое вышло в свет.
На рисунке ниже показаны основные компоненты браузера. Давайте поговорим о том, какую роль они играют в процессе обработки веб-страниц.
Основные компоненты браузера
О различных движках рендеринга
Главная задача движка рендеринга заключается в том, чтобы вывести запрошенную страницу в окне браузера. Движок может выводить HTML-документы, XML-документы, изображения. При использовании дополнительных плагинов движок может визуализировать и материалы других типов, например — PDF-документы.
Мы знаем, что существуют различные JS-движки, которые используют различные браузеры. То же самое справедливо и для движков рендеринга. Вот несколько популярных движков:
- Gecko — используется в браузере Firefox.
- WebKit — применяется в браузере Safari.
- Blink — интегрирован в браузеры Chrome и Opera (с 15-й версии).
Процесс рендеринга веб-страницы
Движок рендеринга получает содержимое запрошенного документа от сетевого уровня браузера. Процесс рендеринга выглядит так, как показано на рисунке ниже.
Процесс рендеринга веб-страницы
Вот основные этапы этого процесса:
- Обработка HTML для создания дерева DOM.
- Создание дерева рендеринга.
- Расчёт параметров расположения элементов дерева рендеринга на экране, формирование макета страницы.
- Визуализация (отрисовка) дерева рендеринга.
Создание дерева DOM
Первый этап работы движка рендеринга заключается в разборе HTML-документа и преобразовании того, что у него получилось, в узлы DOM, размещённые в дереве DOM. При этом веб-страница, которая представлена в виде HTML-кода, преобразуется в структуру, подобную той, которая показана на рисунке ниже.
Каждый элемент этого дерева, содержащий вложенные элементы, является для них родительским. Это справедливо для всех уровней вложенности.
Создание дерева CSSOM
CSSOM (CSS Object Model) — это объектная модель CSS. Когда браузер занимается созданием дерева DOM страницы, он находит в разделе head тег link , который ссылается на внешний CSS-файл, скажем, имеющий имя theme.css . Ожидая, что этот ресурс может понадобиться ему для рендеринга страницы, браузер выполняет запрос на загрузку данного файла. Этот файл содержит в себе обычный текст, представляющий собой описание стилей, применяемых к элементам страницы.
Как и в случае с HTML, движку нужно конвертировать CSS в нечто, с чем может работать браузер — в CSSOM. В результате получается дерево CSSOM, представленное на следующем рисунке.
Дерево CSSOM
Знаете, почему CSSOM имеет древовидную структуру? Когда выполняется формирование итогового набора стилей для элемента страницы, браузер начинает с наиболее общих правил, применимых к этому элементу, представленному узлом DOM (например, если узел является потомком элемента body , к нему применяются все стили, заданные для body ), а затем рекурсивно уточняет вычисленные стили, применяя более специфические правила.
Разберём пример, который представлен на предыдущем рисунке. Любой текст, содержащийся внутри тега span , который помещён в элемент body , выводится красным цветом и имеет размер шрифта, равный 16px . Эти стили унаследованы от элемента body . Если элемент span является потомком элемента p , значит его содержимое не выводится в соответствии с применённым к нему более специфичным стилем.
Кроме того, обратите внимание на то, что вышеприведённое дерево не является полным CSSOM-деревом. Тут показаны лишь стили, которые мы, в нашем CSS-файле, решили переопределить. У каждого браузера имеется стандартный набор стилей, применяемый по умолчанию, известный ещё как «стили пользовательского агента» (user agent styles). Именно результаты применения этих стилей можно видеть на странице, не имеющей связанных с ней CSS-правил. Наши же стили просто переопределяют некоторые из стандартных стилей браузера.
Создание дерева рендеринга
Инструкции о внешнем виде элементов, представленные в HTML, скомбинированные с информацией об их стилизации из дерева CSSOM, используются для формирования дерева рендеринга.
Что это такое? Это — дерево визуальных элементов, созданных в том порядке, в котором они будут выводиться на экран. Это — визуальное представление HTML-кода страницы, отражающее влияние соответствующих этой странице CSS-правил. Цель этого дерева заключается в том, чтобы обеспечить вывод элементов правильном порядке.
Узел дерева рендеринга известен в движке WebKit как «renderer» или «render object» (мы будем называть их «объектами рендеринга»).
Вот как будет выглядеть дерево рендеринга для деревьев DOM и CSSOM, показанных выше.
Дерево рендеринга
Вот общее описание действий браузера, выполняемых им при создании дерева рендеринга.
- Начиная с корня дерева DOM, браузер обходит каждый видимый узел. Некоторые узлы невидимы (например — теги, содержащие ссылки на скрипты, мета-теги, и так далее), их браузер пропускает, так как они не влияют на внешний вид страницы. Некоторые узлы скрыты средствами CSS, браузер так же не включает их в дерево рендеринга. Например, узел span из нашего примера не выводится в дереве рендеринга, так как у нас имеется явным образом заданное правило, устанавливающего для него свойство display: none .
- Для каждого видимого узла браузер находит подходящие CSSOM-правила и применяет их.
- В результате формируется структура, содержащая видимые узлы и вычисленные для них стили.
Формирование макета страницы
После того, как объект рендеринга создан и добавлен в дерево, ему пока ещё не назначены позиция и размер. Вычисление этих значений и называется формированием макета страницы.
HTML использует потоковую модель компоновки. Это означает, что чаще всего система может вычислить геометрические параметры элементов за один проход. Тут используется координатная система, основанная на корневом объекте рендеринга, в ней применяются координаты left и top .
Формирование макета — это рекурсивный процесс. Он начинается в корневом объекте, который соответствует элементу документа . Процесс выполняется рекурсивно по всей иерархической структуре объекта рендеринга, производится вычисление размеров и положения для каждого элемента, который в этом нуждается.
Позиция корневого объекта рендеринга — 0,0 . Его размеры соответствуют размерам видимой части окна браузера (это называют «областью просмотра», viewport).
Процесс формирования макета означает задание каждому узлу точной позиции, в которой он должен появиться на странице.
Визуализация дерева рендеринга
На данном этапе осуществляется обход дерева рендеринга и вызов методов paint() объектов рендеринга, которые и выполняют вывод графического представления объектов на экран.
Визуализация, или отрисовка, может быть глобальной или инкрементной (так же выполняется и формирование макета страницы).
- Глобальная отрисовка означает повторный вывод всего дерева рендеринга.
- Инкрементная отрисовка выполняется в ситуации, когда меняются лишь некоторые из объектов рендеринга, причём так, что это не влияет на всё дерево. Подсистема рендеринга делает недействительными прямоугольные области на экране. Это приводит к тому, что операционная система воспринимает их как участки, содержимое которых нужно обновить и сгенерировать для них событие paint . Операционная система выполняет перерисовку областей интеллектуально, объединяя несколько областей в одну.
Порядок обработки JS-скриптов и CSS-файлов
Разбор и выполнение скрипта осуществляется сразу же после того, как система обработки кода страницы достигнет тега . Обработка документа приостанавливается до тех пор, пока скрипт не будет выполнен. Это означает, что данный процесс выполняется синхронно.
Если скрипт получают из внешнего источника, то сначала он должен быть загружен через сеть (тоже синхронно). Обработка страницы приостанавливается до тех пор, пока загрузка скрипта не будет завершена.
HTML5 позволяет указывать на возможность асинхронной загрузки и обработки скрипта с использованием отдельного потока.
Оптимизация производительности рендеринга
Если вы хотите оптимизировать своё приложение с учётом особенностей рендеринга страниц, существует пять основных областей, которые вы можете контролировать, и на которые нужно обратить внимание.
- JavaScript. В предыдущих материалах этой серии мы рассказывали о том, как писать оптимизированный JS-код, не блокирующий пользовательский интерфейс, эффективно использующий память и реализующий другие полезные техники. Когда речь идёт о рендеринге, нам нужно учитывать то, как JS-код будет взаимодействовать с элементами DOM на странице. JavaScript может вносить множество изменений в пользовательский интерфейс, особенно если речь идёт об одностраничных приложениях.
- Вычисление стилей. Это — процесс определения того, какое CSS-правило применяется к конкретному элементу с учётом соответствующих этому элементу селекторов. После определения правил осуществляется их применение и вычисление итогового стиля для каждого элемента.
- Формирование макета страницы. После того, как браузер узнает о том, какие стили применяются к элементу, он может приступить к вычислению того, как много места на экране займёт этот элемент, и к нахождению его позиции. Модель макета веб-страницы указывает на то, что одни элементы могут влиять на другие элементы. Например, ширина элемента может влиять на ширину дочерних элементов, и так далее. Всё это означает, что процесс формирования макета — это задача, требующая интенсивных вычислений. Кроме того, вывод элементов выполняется на множество слоёв.
- Отрисовка. Именно здесь выполняется преобразование всего, что было вычислено ранее, в пиксели, выводимые на экран. Этот процесс включает в себя вывод текста, цветов, изображений, границ, теней, и так далее. Речь идёт о каждой видимой части каждого элемента.
- Компоновка. Так как части страницы вполне могут быть выведены на различных слоях, их требуется совместить в едином окне в нужном порядке, что приведёт к правильному выводу страницы. Это очень важно, особенно — для перекрывающихся элементов.
Оптимизация JS-кода
JavaScript-код часто приводит к изменению того, что можно наблюдать в браузере. Особенно это актуально для одностраничных приложений. Вот несколько советов, касающихся оптимизации JS для улучшения процесса рендеринга страниц.
- Избегайте использования функций setTimeout() и setInterval() для обновления внешнего вида элементов страниц. Эти функции вызывают коллбэк в некоторый момент формирования кадра, возможно, в самом конце. Нам же нужно вызвать команду, приводящую к визуальным изменениям, в начале кадра, и не пропустить его.
- Переносите длительные вычисления в веб-воркеры.
- Используйте для выполнения изменений в DOM микро-задачи, разбитые на несколько кадров. Этим следует пользоваться тогда, когда задача нуждается в доступе к DOM, а доступ к DOM, из веб-воркера, например, получить нельзя. Это означает, что большую задачу нужно разбить на более мелкие и выполнять их внутри requestAnimationFrame , setTimeout , или setInterval , в зависимости от особенностей задачи.
Оптимизация CSS
Модификация DOM путём добавления и удаления элементов, изменения атрибутов и других подобных действий приведёт к тому, что браузеру придётся пересчитать стили элементов, и, во многих случаях, макет всей страницы, или, по крайней мере, некоторой её части. Для оптимизации процесса рендеринга страницы учитывайте следующее.
- Уменьшите сложность селекторов. Использование сложных селекторов может привести к тому, что работа с ними займёт более 50% времени, необходимого для вычисления стилей элемента, остальное время уйдёт на конструирование самого стиля.
- Уменьшите число элементов, для которых нужно выполнять вычисление стилей. То есть, лучше, если изменение стиля будет направлено на несколько элементов, а не на всю страницу.
Оптимизация макета
Пересчёт макета страницы может требовать серьёзных системных ресурсов. Для оптимизации этого процесса примите во внимание следующее.
- Уменьшите число ситуаций, приводящих к пересчёту макета. Когда вы меняете стили, браузер выясняет, требуется ли пересчёт макета для отражения этих изменений. Изменения свойств, таких, как ширина, высота, или позиция элемента (в целом, речь идёт о геометрических характеристиках элементов), требуют изменения макета. Поэтому, без крайней необходимости, не меняйте подобные свойства.
- Всегда, когда это возможно, используйте модель flexbox вместо более старых моделей создания макетов. Эта модель работает быстрее, чем другие, что может дать значительный прирост производительности.
- Избегайте модели работы с документом, предусматривающей периодическое изменение параметров элементов и их последующее считывание. В JavaScript доступны параметры элементов DOM (вроде offsetHeight или offsetWidth ) из предыдущего кадра. Считывание этих параметров проблем не вызывает. Однако, если вы, до чтения подобных параметров, меняете стиль элемента (например, динамически добавляя к нему какой-то CSS-класс), браузеру потребуется потратить немало ресурсов для того, чтобы применить изменения стиля, создать макет и возвратить в программу нужные данные. Это может замедлить программу, подобного стоит избегать всегда, когда это возможно.
Оптимизация отрисовки
Часто эта задача отнимает больше всего времени, поэтому важно избегать ситуаций, приводящих к перерисовке страницы. Вот что здесь можно сделать.
- Изменение любого свойства, за исключений трансформаций и изменений прозрачности, приводит к перерисовке. Используйте эти возможности умеренно.
- Если ваши действия вызвали пересчёт макета, это приводит и к вызову перерисовки страницы, так как изменения геометрических параметров элемента ведут и к его визуальным изменениям.
- Уменьшайте области страниц, которые необходимо перерисовывать, грамотно управляя расположением слоёв и анимацией.
Итоги
В этом материале мы поговорили о подсистемах рендеринга современных браузеров. Правильный подход к визуализации страниц ведёт к повышению производительности веб-приложений и к улучшению впечатлений пользователей от работы с ними.
Предыдущие части цикла статей:
Итак, Reflow, Repaint и Composition — что же это такое? Большинство фронтенд разработчиков рано или поздно задумываются о том, как же работают веб-браузеры, каким образом происходит отрисовка страницы, что происходит при наведении мышки на элементы и прокликивания кнопок на странице. Reflow, Repaint и Composition, являются основными этапами отвечающими за отрисовку страницы в веб-браузере. Понимание как они работают и, что вызывает срабатывание того или иного процесса, позволит создавать более быстрые и эффективные веб-приложения.
Но перед тем как мы приступим к разбору этих этапов, давайте пробежимся по тем процессам, которые происходят до начала отрисовки страницы, после получения html страницы с сервера:
- Html парсится и разбивается на токены;
- Токены преобразуются в Ноды;
- Ноды собираются в DOM (Domain Object Model) дерево;
- Параллельно с этим процессом происходит парссинг CSS правил и строится CSSOM (CSS Object Mode );
- CSSOM и DOM объединяются в Render Tree, похожее на DOM дерево, в котором скрываются не видимые элементы ( meta , script , link и display: none ) и добавляются элементы которых нет в DOM (псевдоэлементы :before, :after);
- Далее начинаются процессы, которым посвящена данная статья: Reflow, Repaint и Composition.
Итак, после того как браузер определил структуру документа и стили для каждой ноды, начинается Reflow. Хотя правильнее будет сказать Layout для первой операции, а все дальнейшие повторные операции, будет правильнее называть Reflow. Иногда этот процесс называется просто Layout, во всех случаях.
Что же происходит на этом этапе? Для того чтобы отобразить элементы на странице, браузеру необходимо понимать какого размера эти элементы и где они располагаются. Браузер проходит по рендер дереву (Render Tree), начиная от корневого элемента дерева и определяет координаты каждого элемента и пространство которое он занимает. Таким образом создается Layout Tree. Для элементов у которых display: none, вычисления не будут производиться, тем не менее элементы с visibility: hidden будут участвовать в Layout/Reflow, так же как и псевдоэлементы, у которых имеется заполненное свойство content (div::before), хотя их нет в DOM. Определение размеров и местоположений элементов происходит не за один проход по дереву, проход может происходить несколько раз, если элементы встречающиеся позже, влияют на предыдущие элементы. В целом это очень сложная процедура, требующая значительных вычислительных ресурсов.
Каждый раз когда вы меняете свойство стиля, которое отвечает за положение и размеры элемента, происходит повторный процесс расчета размеров и положений у всех элементов, на которые могут повлиять эти изменения, происходит Reflow. Reflow’у могут быть подвергнуты как отдельные ветки Рендер дерева (Render Tree), так и все дерево целиком. Чем глубже вложенность элемента, тем больше элементов будут затронуты при Reflow, поэтому необходимо сохранять структуру вашего документа как можно более плоской. Так же для уменьшения количества Reflow операций, следует уменьшить количество CSS правил и избавиться от правил которые вы не используете.
Ниже список наиболее популярных свойств, изменение которых вызывает Reflow:
Layout / Reflow происходит в основном потоке браузера, то есть там же где исполняется JS, крутится Event Loop и т. д. Поэтому в моменты когда у вас исполняется тяжелый код в JS, Reflow будет блокирован, как следствие интерактивность вашей страницы также будет заблокирована.
В этом видео визуализирован Reflow:
Следующий этап после Layout/Reflow называется Paint или Repaint для последующих повторных операций. Размера и положения элементов не достаточно для того чтобы отобразить страницу, нужно знать каким образом «покрасить» эти элементы. На этапе Paint/Repaint браузер обходит Layout Tree и создает записи о том как будут отрисованы элементы на странице(позиция x,y, ширина, высота, цвет).
После того как браузер знает размеры, позицию и то в какой последовательности красить элементы, наступает пора конечной отрисовки элементов на странице. Для этого браузер на этапе Composite группирует различные элементы по слоям, растрирует эти слои, то есть отрисовывает пиксели и затем объединяет эти слои в готовую страницу в отдельном потоке композитора (compositor thread ). Все это делается для повышения производительности страницы. Теперь при скролле страницы достаточно просто сдвинуть в необходимый отрисованный слой и заново объединить слои в потоке композитора.
Так как размеры слоев могут быть достаточно большими, поток композитора также разделяет их на части (Tiles) и отрисовывает в первую очередь те части, которые видны в данный момент в окне браузера.
В этой статье я попытался вам рассказать о процессах Reflow, Repaint, Composite. Изучив как они работают, можно сделать вывод о том, как важно сохранять более плоскую структуру вашего html документа, минимизировать изменения элементов и обращения через JS, а так же почему следует использовать свойства transform, opacity, will-change для более плавной анимации.
На собеседованиях мы часто просим кандидата рассказать настолько подробно, насколько он может, что происходит, когда вводишь в адресной строке браузера адрес сайта и нажимаешь кнопку “Ввод”. В зависимости от того, кого собеседуем — фронтендщика или бекендщика — мы ожидаем разные ответы. А как бы выглядел идеальный ответ на этот вопрос? Ниже мой вариант ответа.
Итак, пользователь вводит в адресной строке браузера адрес сайта и нажимает кнопку “Ввод”.
Браузер состоит из нескольких компонентов, одним из которых является User Interface. Адресная строка как раз является одной из частей этого компонента.
User Interface после ввода URL в адресной строке передаёт управление компоненту Browser Engine, который отвечает за взаимодействие различных компонентов браузера.
Чтобы сделать запрос по указанному URL, браузеру нужно знать IP сервера. Первым делом он смотрим в свой локальный кэш DNS.Компонент Browser Engine как раз имеет доступ к этому кэшу.
Если там нет соответствующей записи, то браузер передаёт управление операционной системе, которая проверяет свой кэш DNS. Если и там отсутствует соответствующая запись, то ОС смотрит в локальные хосты (файл /etc/hosts в Unix-системах). Если запись о хосте отсутствует, то операционная система обращается к интернет провайдеру, у которого тоже есть свой кэш DNS на своих рекурсивных серверах DNS. В случае отсутствия записи в кэше на серверах DNS провайдера, запрос идёт на корневой DNS. У корневого DNS тоже есть кэш. Если соответствующей записи в кэше корневого DNS нет, запрос идёт дальше по цепочке серверов DNS.
Если на любом из этапов находится нужная запись, то она сохраняется во всех кэшах и управление возвращается браузеру, который уже знает IP нужного сервера.
Процесс получения IP адреса называется DNS lookup.
На сервере запрос принимает веб-сервер (например, nginx или apache).
В конфигурационных файлах веб-сервера прописаны обслуживаемые хосты. Веб-сервер достаёт хост из заголовка запроса host и сопоставляет с теми, которые указаны в конфигурации. Если есть совпадение, то веб-сервер находит в конфигурационном файле правила обработки такого запроса и выполняет их. Дальнейшее поведение сервера зависит от технологии и особенностей приложения. Здесь может происходить работа с базами данных, кэшами, запросы к другим серверам и сервисам, выполнение различных скриптов. Для простоты представим, что приложение сгенерировало файл HTML, и веб-сервер отдал его браузеру.
Заголовки ответа сервера можно увидеть в Chrome DevTools на вкладке Networking, выбрав нужный запрос
Если длина контента больше нуля и тип контента поддерживается браузером, то браузер пытается его обработать. В нашем случае браузер получает файл HTML с соответствующим заголовком Content-Type. Браузер начинает разбор (parsing) этого файла с первой инструкции, которой является инструкция . DOCTYPE указывает на версию HTML, чтобы браузер понимал, каким правилам следовать во время разбора (какие теги как обрабатывать).
Если DOCTYPE отсутствует, то браузер переключится в режим quirks mode и попытается разобрать документ HTML, однако многие элементы будут проигнорированы. Если указан корректный DOCTYPE, то браузер будет работать в standards mode и будет разбирать документ в соответствии с правилами той версии, которая указана в DOCTYPE.
Rendering Engine начинает разбор документа HTML.
Создаётся DOM (Document Object Model). В браузере этот объект доступен по ссылке, которая хранится в переменной document. У документа есть несколько состояний. Первое состояние — loading. Оно означает, что документ только начал формироваться.
Состояние документа хранится в переменной document.readyState.
Также создаётся объект styleSheets, который будет хранить все стили.
Все стили на странице доступны по ссылке, которая хранится в переменной document.styleSheets.
Любой файл — это набор байтов. Браузер берёт полученный набор байтов и преобразует их в символы по таблице символов в соответствии с кодировкой, которая была передана в заголовке Content-Type. В нашем примере это кодировка UTF-8.
Следующий процесс —разбивание текста на смысловые блоки (tokenization). Так браузер распознаёт теги , и проч., а также понимает, какие правила к какому тегу применять (например, поддерживаемые атрибуты).
Далее токены собираются в узлы (nodes). Эти узлы и сохраняются в DOM со всеми взаимными связями.
Во время разбора, если Rendering Engine встречает ссылку на внешний ресурс, то он передаёт команду загрузить этот ресурс компоненту Networking Component. Это может быть ссылка на стили, скрипты, картинки и т.п. Networking Component ставит все ресурсы в очередь на загрузку. Каждому ресурсу Networking Component присваивает приоритет.
Приоритеты ресурсов можно посмотреть в Chrome DevTools на вкладке Networking в колонке Priority.
Так, у HTML, CSS и шрифтов самый высокий приоритет. У изображений приоритет изначально низкий, но если Rendering Engine обнаружит, что изображение попадает в поле видимости (view port) пользователя, то повысит приоритет до среднего. Приоритет скрипта зависит от положения на странице и способа загрузки. У асинхронных скриптов (async/defer) низкий приоритет. У скриптов, которые в документе перед изображениями — высокий, у тех, что после хотя бы одного изображение — средний.
По возможности браузер пытается загружать ресурсы параллельно. Однако, он не может загружать параллельно более 6 ресурсов с одного домена.
Кроме того, когда Rendering Engine отдаёт команду компоненту Networking Component на синхронную загрузку стиля или скрипта, он останавливает разбор документа.
С загрузкой стилей происходит подобный процесс преобразования из байтов в Object Model (CSSOM): байты -> символы -> токены -> узлы -> CSSOM.
Немного иначе происходит загрузка скрипта. Вместо того, чтобы вернуть управление Rendering Engine’у, Networking Component . передаёт управление JavaScript Interpreter, который преобразует байты в исполняемый код: байты -> символы -> токены -> Abstract Syntax Tree (evaluating). Далее в работу вступает компилятор, который оптимизирует AST, кэширует некоторые участки кода, компилирует его на лету (JIT compilation) в исполняемый код и исполняет (executing). Однако исполняется скрипт только, когда готова CSSOM. До тех пор скрипт стоит в очереди на исполнение.
Во многих современных браузерах во время исполнения JavaScript в отдельном потоке продолжается сканирование документа на наличие ссылок на другие ресурсы и постановка ресурсов в очередь на скачивание (Speculative parsing).
Каждый этап разбора HTML, CSS и JS можно увидеть в Chrome DevTools во вкладке Performance
Если при загрузке скрипта Rendering Engine видит у скрипта атрибут async, то он не останавливает разбор документа во время загрузки скрипта. Скрипт также станет в очередь на исполнение, дожидаясь, когда CSSOM будет готова.
Если при загрузке скрипта Rendering Engine видит у скрипта атрибут defer, то он не останавливает разбор документа во время загрузки скрипта, но когда скрипт загрузится, он станет в очередь на исполнение, которая заработает при возникновении события DOMContentLoaded. К этому моменту CSSOM будет уже готова.
Когда Rendering Engine заканчивает разбор документа, он вызывает событие DOMContentLoaded, и состояние документа меняется на interactive. При этом ресурсы (например, картинки) могут продолжать загружаться.
Когда все ресурсы загрузились, вызывается событие load, а состояние документа меняется на complete.
После того, как документ полностью разобран и сформированы DOM и CSSOM, Rendering Engine начинает построение Render Tree. В него попадут все элементы, которые нужно отрисовать. Некоторые элементы изначально могут быть невидимыми — их не нужно рисовать. Для каждого элемента, который “выпадает” из потока (например, используется position: absolute), будет создаваться отдельная ветка в Render Tree.
Во время Rendering Tree происходит сопоставление узлов из DOM и узлов CSSOM.
Свойства узла можно получить с помощью функции window.getComputedStyles(узел).
Когда Rendering Tree готов, Rendering Engine запускает процесс layout. Он заключается в вычислении размеров и позиций каждого элемента на странице.
Следующий этап — paint. Rendering Engine вычисляет цвет каждого пикселя.
И, наконец, последний этап — composite. Компонент UI Backend слой за слоем отрисовывает элементы на странице. При этом, если требуется отрисовать изображение, которое ещё не загрузилось, во время процесса layout, Rendering Engine зарезервирует место для изображения, если у него указаны ширина и высота. Rendering Engine вынесет на отдельный слой те элементы, стили которых содержат правила opacity, transform или will-change. Более того, эти слои Rendering Engine передаст для обработки GPU.
Если требуется отобразить текст, для которого используется нестандартный шрифт, то современные браузеры скроют текст до момента загрузки шрифта (flash of invisible text).
В современных браузерах скачивание документа, его разбор и отрисовка происходят по кускам, частями.
В документе HTML могут присутствовать некоторые мета-теги, которые могут менять порядок загрузки ресурсов, а также их приоритет.
К примеру, мета-тег dns-prefetch вынуждает Rendering Engine обратиться к Networking Component и получить IP нужного домена ещё до того, как Rendering Engine встретить его в документе.
Мета-тег prefetch вынудит Networking Component поставить указанный ресурс в очередь на загрузку с низким приоритетом.
Мета-тег preload вынудит Networking Component поставить указанный ресурс в очередь на загрузку с высоким приоритетом.
Мета-тег preconnect вынудит Networking Component заранее подключиться к другом хосту, то есть пройти нужные этапы: DNS lookup, redirects, hand shakes.
Время, которое затрачивается на каждый этап, происходящий во время запроса и разбора документа HTML, хранится в объекте performance.timing.
Читайте также: