Вы вводите название сайта в адресную строку браузера, нажимаете enter, и по привычке видите запрашиваемую страницу. Все просто: ввел название сайта — сайт отобразился. Однако для более любознательных хочу рассказать, что происходит между тем как браузер начинает получать куски сайта (да, сайт приходит кусками, по-другому — чанками) и отображает полностью нарисованную страницу.
![](https://habrastorage.org/r/w1560/webt/ss/tk/lb/sstklb7ifbwb0rdaucjpytjm1vk.png)
Перед историей о том, как браузер рисует страницу, важно понять как он устроен, какие процессы и на каком уровне выполняются. При знакомстве с процессом рендеринга мы не раз вспомним о компонентах браузера. Итак, под капотом браузер выглядит примерно следующим образом:
![](https://habrastorage.org/r/w1560/webt/dl/fr/hh/dlfrhh43eqb66arhgeic_h6e_8c.png)
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. Другими словами …
Итак, с помощью компонента Network браузер начал получать html-файл чанками обычно по 8кб, что дальше? А далее идет процесс парсинга (спецификация процесса) и рендеринга этого файла в компоненте, как вы уже догадались — Rendering Engine.
Сам процесс парсинга выглядит так:
![](https://habrastorage.org/r/w1560/webt/64/6h/n1/646hn1rs70cjwv3g64bolqeemq4.png)
Результатом парсинга является DOM дерево. Возьмем к примеру такой html:
DOM дерево такого html файла будет выглядеть так:
![](https://habrastorage.org/r/w1560/webt/us/hs/wc/ushswctwocrvf7xbpmwvylr0qbs.png)
По мере того как браузер парсит html файл, он встречает теги содержащие ссылки на сторонние ресурсы (<link>, <script>, <img> и так далее) — по мере их обнаружения происходит запрос за этими ресурсами.
Таким образом, отправив запрос по адресу прописанному в атрибуте href тега<link rel="stylessheet"> и получив файл css стилей, браузер парсит этот файл и строит так называемый CSS Object Model — CSSOM.
Представим что у нас есть такой файл стилей:
Из которого получим такой CSSOM:
![](https://habrastorage.org/r/w1560/webt/xw/aw/dk/xwawdku6z5xbkgpozpfdrksvvjw.png)
Подробнее об алгоритме парсинга 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:
![](https://habrastorage.org/r/w1560/webt/ga/a3/oc/gaa3ocbwb0yzdmzvcagidmu8co0.png)
На данный момент мы имеем в некотором состоянии 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».
К концу процесса layout каждый render object имеет свое положение и размеры.
Подводя промежуточный итог: браузер знает что, как и где рисовать. Следовательно — осталось только нарисовать. Этот процесс, как ни странно, называется Paint.
Paint — этап, где пиксель монитора заполняется цветом указанным в свойствах render object'а и белый экран превращается в картину задуманную автором (разработчиком). На всем пути рендеринга — это самый дорогой процесс (не то чтобы предыдущее дешевые).
Также, как и процесс layout, отрисовка (paint) может быть глобальной — дерево перерисовывается полностью, и инкрементальной — дерево перерисовывается частично. Для частичного перерисовывания render object помечает свой rectangle как невалидный. Операционная система расценивает эту область как требующую перерисовки и вызывает событие paint. При этом браузер умеет объединять области, чтобы выполнить разом перерисовку для всех мест, где это необходимо.
Значит, должно быть что-то, что помогло бы создавать сайты с богатой анимацией, при этом не нагружая CPU и рисуя каждый кадр менее чем за 16,6мс (60 fps). Действительно, браузер выполняет еще один этап, который помогает оптимизировать динамику сайтов — Composite (композиция).
Перед композицией, все нарисованные элементы находятся на одном слое (memory layer). То есть, изменение параметров (например, геометрических размеров или положения) одних элементов повлекут перерасчет параметров соседних элементов. Но если распределить элементы на композиционные слои — изменение параметров элемента вызовут перерасчет только на определенном слое, не затрагивая при этом элементы на других слоях. Таким образом, этот процесс является самым дешевым по производительности, поэтому нужно стараться вносить изменения вызывающие только composite.
Резюмируя вышесказанное, получаем такой процесс рендеринга веб страницы:
![](https://habrastorage.org/r/w1560/webt/vy/30/eh/vy30eha1riss2agnsvnzbk2bwlq.png)
TLDR;
Браузер получает html файл, парсит его и строит DOM. Встречая css стили, браузер их подгружает, парсит, строит CSSOM и объединяет вместе с DOM'ом — получаем Render Tree. Осталось выяснить где расположить элементы из Render Tree — этим занимается задача layout. После расположения элементов, можно начать рисовать их — это задача paint, этап на котором заполняются пиксели экрана.
Что происходит когда изменяется css свойство? Или, например, добавляется новый dom узел? В случае изменения css свойств все зависит от изменяемого свойства. Есть только два свойства которые вызывают задачу composite — это opacity и transform. Только эти два свойства являются самыми дешевыми для анимации. К примеру, изменение background вызовет задачу paint (затем composite), а изменение display вызовет сначала layout, далее paint, после чего composite. Список задач, которые вызываются изменениями стилей можно посмотреть на csstriggers.com.
При добавлении новой ноды в dom дерево — очевидно браузеру нужно добавить новый объект в дерево, посчитать его положение на странице, посчитать положения других элементов на странице (если они были аффектнуты новым элементом), и в конце все это нарисовать — звучит дорого. Поэтому делая такие операции необходимо иметь в виду производительность, ведь не каждый пользователь интернета запускает ваше веб-приложение на самой последней модели устройства.
Подводя итог, мы рассмотрели из каких компонентов состоит браузер, как они взаимодействуют друг с другом и как Rendering Engine рисует страницу пользователю.
Посмотреть вышеописанное можно в devtools'ах хрома, но чтобы не выходить за рамки названия статьи — на этом пока все.
![](https://habrastorage.org/webt/ss/tk/lb/sstklb7ifbwb0rdaucjpytjm1vk.png)
Как устроен браузер?
Перед историей о том, как браузер рисует страницу, важно понять как он устроен, какие процессы и на каком уровне выполняются. При знакомстве с процессом рендеринга мы не раз вспомним о компонентах браузера. Итак, под капотом браузер выглядит примерно следующим образом:
![](https://habrastorage.org/webt/dl/fr/hh/dlfrhh43eqb66arhgeic_h6e_8c.png)
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. Вместо этого браузер сразу пытается отобразить пользователю страницу (далее рассмотрим как).
Сам процесс парсинга выглядит так:
![](https://habrastorage.org/webt/64/6h/n1/646hn1rs70cjwv3g64bolqeemq4.png)
Результатом парсинга является DOM дерево. Возьмем к примеру такой html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Web Rendering</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="wrapper">
<div class="header">
<h1>Hey</h1>
</div>
<div class="content">
<p>
Lorem <span>ipsum</span>.
</p>
</div>
<footer>
Contact me
</footer>
</div>
<script src="./code.js"></script>
</body>
</html>
DOM дерево такого html файла будет выглядеть так:
![](https://habrastorage.org/webt/us/hs/wc/ushswctwocrvf7xbpmwvylr0qbs.png)
По мере того как браузер парсит html файл, он встречает теги содержащие ссылки на сторонние ресурсы (
Таким образом, отправив запрос по адресу прописанному в атрибуте href тега
Представим что у нас есть такой файл стилей:
body {
font-size: 14px;
}
.wrapper {
width: 960px;
margin: 0 auto;
}
.wrapper .header h1 {
font-size: 26px;
}
.wrapper p {
color: red;
}
footer {
padding: 20px 0;
}
Из которого получим такой CSSOM:
![](https://habrastorage.org/webt/xw/aw/dk/xwawdku6z5xbkgpozpfdrksvvjw.png)
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:
![](https://habrastorage.org/webt/ga/a3/oc/gaa3ocbwb0yzdmzvcagidmu8co0.png)
На данный момент мы имеем в некотором состоянии 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.
Резюмируя вышесказанное, получаем такой процесс рендеринга веб страницы:
![](https://habrastorage.org/webt/vy/30/eh/vy30eha1riss2agnsvnzbk2bwlq.png)
TLDR;
Браузер получает html файл, парсит его и строит DOM. Встречая css стили, браузер их подгружает, парсит, строит CSSOM и объединяет вместе с DOM'ом — получаем Render Tree. Осталось выяснить где расположить элементы из Render Tree — этим занимается задача layout. После расположения элементов, можно начать рисовать их — это задача paint, этап на котором заполняются пиксели экрана.
Динамика
Что происходит когда изменяется css свойство? Или, например, добавляется новый dom узел? В случае изменения css свойств все зависит от изменяемого свойства. Есть только два свойства которые вызывают задачу composite — это opacity и transform. Только эти два свойства являются самыми дешевыми для анимации. К примеру, изменение background вызовет задачу paint (затем composite), а изменение display вызовет сначала layout, далее paint, после чего composite. Список задач, которые вызываются изменениями стилей можно посмотреть на csstriggers.com.
При добавлении новой ноды в dom дерево — очевидно браузеру нужно добавить новый объект в дерево, посчитать его положение на странице, посчитать положения других элементов на странице (если они были аффектнуты новым элементом), и в конце все это нарисовать — звучит дорого. Поэтому делая такие операции необходимо иметь в виду производительность, ведь не каждый пользователь интернета запускает ваше веб-приложение на самой последней модели устройства.
Подводя итог, мы рассмотрели из каких компонентов состоит браузер, как они взаимодействуют друг с другом и как Rendering Engine рисует страницу пользователю.
Посмотреть вышеописанное можно в devtools'ах хрома, но чтобы не выходить за рамки названия статьи — на этом пока все.
Источники
- https://blog.algolia.com/performant-web-animations/
- https://www.zeolearn.com/magazine/components-of-web-browsers
- https://www.html5rocks.com/ru/tutorials/internals/howbrowserswork
- https://blog.sessionstack.com/how-javascript-works-the-rendering-engine-and-tips-to-optimize-its-
performance-7b95553baeda - https://developers.google.com/web/fundamentals/performance/rendering