Самым важным для Frontend разработчика является модуль отображения в браузере, он же Rendering Engine (далее RE).

В этой статье я хочу взять простую страничку и пройти все этапы вместе с RE от получения первого байта и до отрисовки контента на экран. Пользоваться я, как всегда, буду браузером Chrome.

Для начала разберем, из каких еще модулей состоит браузер, чтобы понимать с чем взаимодействует RE.

Рассмотрим схему:


Рисунок 1

Пользовательский интерфейс, User Interface (далее UI) — внешний API браузера для пользователя: адресная строка, навигация, меню, закладки, кнопки 'обновить' и 'домой'.

Механизм браузера, Browser Engine (далее BE) прослойка между пользовательским интерфейсом и модулем отображения.

Модуль отображения Rendering Engine. Его подробнее разберем позже.

Сетевые компоненты, Network отвечают за запросы по сети. RE получает данные от Network. Данные принимаются порциями по 8Кб и RE не ждет, пока придут все данные, он начинает обрабатывать их по мере поступления.

Модуль JS Interpreter отвечает за интерпретацию скрипта и его выполнение.

UI backend применяется для отрисовки основных графических элементов и виджетов, типа окон и комбо-боксов. Простой пример окно alert или prompt.

Xранилище данных — это cookie, indexDB и другие хранилища браузера.

Теперь, когда мы знаем на базовом уровне, из чего состоит браузер, можем перейти к интересующему нас компоненту — Rendering Engine.

Разбираться проще и быстрее на конкретном примере, поэтому давайте возьмем простую html-страничку с одним внешним css- и js-файлом (скрипт подключен с атрибутом async, далее разберем почему). И посмотрим, как RE их обрабатывает и какие шаги выполняются, прежде чем мы увидим нужный нам контент на экране.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="stylesheet" href="./style.css"></link>
  <title>Document</title>
</head>
<body>
  <div>Hello Habr!</div>
  <div>I'am Rendering Engine</div>

  <script async src="./script.js"></script>
</body>
</html>

(function() {
  window.addEventListener('load', () => {
    console.log('all resources were loaded');
  });
})();

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: Helvetica, sans-serif;
  line-height: 1.5;
  background-color: #9BD4F4;
  padding: 16px;
}

Для этого заходим в Chrome DevTools, открываем вкладку perfomance и запускаем процесс. После перезагрузки страницы и анализа произошедшего мы наблюдаем следующую картину:


Рисунок 2

Во вкладке Network — последовательность загрузки данных по сети (голубой прямоугольник — index.html).

Во вкладке Timings — отметки, когда произошли события DCL (DOM Content Loaded, FP — first paint, FCP — first contentful paint, FMP — first meaningful paint, L — load). Давайте разберем, что это за события.

DOMContentLoaded — браузер загрузил HTML, распарсил его и построил DOM-дерево. Это событие срабатывает на document, на него легко можно подписаться и работать с DOM через JavaScript (в нашем скрипте мы не сможем подписаться на событие DOMContentLoaded, так как оно произошло до того, как распарсился скрипт, см рисунок).
Также у DOMContentLoaded есть несколько нюансов:

  • Если скрипт подключен без тегов async / defer (синхронно), то он будет блокировать парсинг HTML. Однако браузеры в последнее время используют спекулятивный парсинг и в таких случаях все равно скачивают этот скрипт заранее и делают его синтаксический анализ. Это никак не влияет на структуру DOM-дерева, но позволяет сократить время работы RE. На рисунке ниже видно, как время DCL и всех остальных render events увеличивается при синхронном подлючении скриптов
  • Блокировку парсинга (как вы уже догадались) можно обойти атрибутами async / defer, которые позволяют продолжать парсить HTML, не дожидаясь скачивания и выполнения скрипта
  • Также событие DCL может отложиться из-за загрузки стилей. Во время выполнения скрипта браузер может увидеть, что мы хотим получить доступ к стилю элемента через JavaScript. И этот скрипт будет заблокирован при условии, что стили этого элемента в данный момент парсятся или загружаются
  • Также в Chrome, например, на DCL происходит автозаполнение форм.


Рисунок 3

First paint — браузер отрендерил первый пиксель на странице.

First contentful paint — браузер отрендерил первый контент на странице.

First meaningful paint — событие отрабатывает после того, как RE определит, что отрендеренный контент может быть полезен пользвателю.

Load вся страница и ресурсы на ней загружены, включая iframe.

Об FP, FCP, FMP отлично написано в официальной документации Google for developers.

Теперь, когда мы разобрались, какие события произошли, можем перейти к Сall tree (см Рисунок 1) и более подробно разобрать, когда и почему эти события происходят.

Parse HTML — парсинг HTML. Про это можно написать отдельную статью. А еще лучше почитать спеку Нам лишь нужно понять, что браузер на основе HTML создает у себя объектную модель документа — DOM. И, когда она готова и ничего больше не может на нее повлиять, отрабатывает событие DOMContentLoaded.

Composite layers — это объединение визуальных элементов из отдельных источников в единые изображения для создания иллюзии, что все эти элементы являются частями одной и той же сцены.

Recalculate style. Любые изменения DOM, будь то добавление или удаление элементов, изменение атрибутов, классов или использование средств анимации, ведут к тому, что браузер перерасчитывает стили элементов и во многих случаях макет всей страницы или ее частей. Этот процесс называется вычислением стилей. Google for developers

Parse Style sheet. Если после синтаксического анализа RE видит, что в HTML подключен css, он начинает его заранее скачивать и парсить. После парсина RE строит CSS Object Model — объектную модель CSS.

Далее происходит этап attachment, при котором RE сопоставляет CSS OM и DOM, и мы получаем Render Tree.

Update layer tree (Layout) — компоновка дерева слоев или просто компоновка. После того, как мы сопоставили CSS OM и DOM, можем узнать местоположение элементов и их размеры.

Чаще всего элементы, которые идут ниже в потоке, не могут оказывать влияние на позиционирование элементов выше, поэтому компоновка чаще всего выполняется последовательно — сверху вниз и слева направо. Поэтому HTML-стандартом предусмотрена поточная модель компоновки документа.

Paint — отрисовка содержимого на экран. И только после всех этих шагов мы видим контент сайта у себя на экране :D

Вот краткая схема всех этапов работы RE:


Рисунок 4

Резюмируем:

Данные в RE поступают из сетевого модуля порциями. Получая эти данные, RE начинает с ними работать, а именно — парсить HTML.

Когда RE видит, что в HTML встречается внешний ресурс, он говорит об этом Network, и тот начинает его скачивать и дальше снова отдает его RE.

Встречая тег <script> по стандарту RE прекращает парсинг и ждет, пока этот скрипт скачатеся и выполнится, и только потом продолжает парсинг и построение DOM-дерева. Это решается атрибутами async / defer. Про их отличия подробнее можно почитать тут Главное понять, что они дают возможность продолжить парсить HTML, не дожидаясь обработки скрипта.
Также браузеры (в нашем случае Chrome) могут блокировать выполнение скрипта, если он пытается работать (через JavaScript) с css элемента, стили которого в данный момент обрабатываются.

После того как RE понимает, что все синхронные скрипты скачались и отработали, HTML полностью распарсился и нам больше ничего не мешает, он вызывает событие DOMContentLoaded, и мы получаем в браузере объект #document, с которым можно работать.

Далее, после завершения парсинга CSS и конструирования CSS Object Model, происходит этап attachment, где строится Render Tree и происходит Layout (компоновка размеров и положения блоков). Ну а после Layout происходит отрисовка на экран — Paint. Такой длинный путь проделывает Rendering Engine, чтобы мы с вами увидели это:


Рисунок 5

Вот и все!

Надеюсь, эта статья была вам полезной и теперь вы понимаете, как работает Rendering Engine.

Всем пока :) И до новых встреч. Если вам понравилось, ставьте лайки и подписывайтесь на мой канал :)

Комментарии (4)


  1. dayllenger
    21.07.2019 21:26
    +1

    P.S. Так как браузер парсит CSS-селекторы справа-налево, то не рекомендуется использовать сложные вложенные селекторы типа div div div, .class div a и т. д. Это может сильно нагрузить браузер. Чем проще селектор, тем лучше.

    Дело ведь не в самом парсинге, парсятся-то селекторы быстро и только один раз. Затраты идут на нахождение каскада из правил, которые действуют на элемент. Например, есть у нас правило с селектором вида A B, и есть элемент. Нам нужно взять правый селектор, сопоставить его с элементом. Если успех, то взять оставшуюся часть селектора, т.е. A, и пройти вверх по DOM-у до самого корня, сопоставляя её с каждым предком. И это уже занимает время.


    1. anatoliy841993 Автор
      21.07.2019 23:21

      спасибо за замечание, изначально не хотел писать про это, в итоге оставил, да еще и с ошибкой) устранил неточность


  1. Bakkondy
    22.07.2019 11:38

    крутая статья!!!


    1. anatoliy841993 Автор
      22.07.2019 11:39

      спасибо!