Как браузер рисует страницы
При загрузке сайта, браузер выполняет множество операций, которые так или иначе влияют на отрисовку страницы. Эти операции в целом можно поделить на несколько стадий.
- Загрузка основного ресурса
- Получение внешних ресурсов
- Выполнение JavaScript
- Построение Render-дерева
- Расчёт макета (Layout)
- Отрисовка документа (Paint)
- Объединение слоёв (Composite)
Загрузка основного ресурса
Под основным ресурсом подразумевается точка входа в веб-приложение. В большинстве случаев это обычный index.html
файл.
После загрузки изначального ресурса, браузер начинает парсинг HTML-файла, выделяя "токены", которые представляют собой начало тега, конец тега и его содержание.
Из всего анализа документа, браузер строит так называемое DOM-дерево.
Получение внешних ресурсов JavaScript
Помимо HTML ресурсов, веб-приложение так-же может задействовать внешние ресурсы, например такие, как CSS или JS файлы, картинки, а так-же аудио-файлы.
Загрузка JavaScript-файлов
Как только браузер доходит до инициализации JavaScript-файлов, он блокирует парсинг HTML. После чего начинается скачивание и выполнение скриптов.
Однако в наше время загрузка скриптов выглядит иначе и браузер, доходя до первого скрипта, начинает искать все остальные скрипты и скачивать их одновременно. Важно подметить, что хоть скрипты загружаются одновременно, запуск всё равно идёт последовательно.
Почему же загрузка скриптов JS требует остановки парсинга? Дело в document.write
, который работает только на этапе парсинга и модифицирует исходный документ в том месте, где был вызван скрипт и заблокирован парсер.
Атрибуты async и defer
Мы можем влиять на порядок загрузки скриптов посредством использования атрибутов async
и defer
. В случае с async
, скрипт будет запрошен сразу и будет блокировать парсинг только на этапе выполнения скрипта. Соответственно остальные асинхронная загрузка скрипта не даёт гарантии, что он загрузится в правильном порядке относительно остальных скриптов.
При использовании defer
, скрипт выполнится сразу после парсинга страницы, не блокируя его. При чём скрипт будет сохранять порядок выполнения для всех остальных defer
скриптов в вызываемой последовательности.
- Каждый предыдущий defer-скрипт будет блокировать следующий.
defer
откладывает событие DOMContentLoaded, в отличии отasync
Получение внешних ресурсов CSS
В отличии от получения JavaScript ресурсов, стили не блокируют парсинг HTML-файла, однако блокируют отрисовку страницы.
Рекомендуется инициализировать CSS-файлы до скриптов, чтобы браузер грузил стили в первую очередь. В противном случае, браузер начнет скачивать стили только после того, как скачает и выполнит JS-код, что соответственно отложит отрисовку.
Создание CSSOM
После загрузки стилей, браузер строит из полученных ресурсов так называемый CSSOM (Cascade Style Sheets Object Model). Это что-то похожее на DOM, только для стилей, где для каждого стиля есть соответствующий узел.
- CSS - блокирующий обработку ресурс, соответственно Render Tree не будет построено без полного построения CSSOM.
- Парсинг CSS, в отличии от HTML, не может работать по частям в силу своих особенностей. Ведь в HTML-документе мы можем переопределять и изменять стили, написанные выше. Поэтому, чтобы перейти к следующему шагу, необходимо полностью преобразовать CSS.
Выполнение JS кода
После переломного момента, когда HTML загружен, преобразован в DOM, а JavaScript спаршен, генерируется так называемое событие DOMContentLoaded
.
Для любого сценария, задействующего модель DOM, рекомендуется сначала дождаться этого события.
document.addEventListener('DOMContentLoaded', () => { // Код, задействующий модель DOM });
После завершения загрузки HTML со всеми внешними ресурсами (картинки, стили, асинхронный JS и т.д.), срабатывает событие браузера, под названием load.
window.addEventListener('load', () => { // Страница полностью загружена });
Помимо этого, мы можем явно проследить за состоянием загрузки страницы с помощью свойства document.readyState
. Он имеет 3 возможных значения:
"loading"
- документ на стадии загрузки"interactive"
- документ полностью прочитан"complete"
- документ полностью прочитан и внешние ресурсы загружены
document.addEventListener('readystatechange', () => { console.log('readyState: ', document.readyState); });
Важное уточнение, что document.readyState
станет "interactive"
прямо перед событием DOMContentLoaded
. Эти две вещи, на самом деле, обозначают одно и то же. Так-же соответствует "complete"
для window.onload
.
Построение Render-дерева
После всех вышеизложенных действий, браузер начнет строить так называемое Render-дерево. Оно представляет собой сочетание DOM и CSSOM, однако не все узлы попадут в итоговое рендер дерево.
В него не попадут узлы со свойством display: none
, однако узлы со стилями opacity: 0
или visibility: hidden
включены будут. Так-же в него не попадут теги, не содержащие визуальной информации, такие как <head />
Расчёт макета (Layout)
После создания Render-дерева, браузер начинает позиционировать элементы на странице. Для этого он раcсчитывает размеры и положение каждого элемента рекурсивно. Расчёт начинается от корневого элемента Render-дерева, размер которого равен размеру viewport'а
.
Layout также может быть глобальным, когда требуется расcчитать положение render объектов всего дерева, и инкрементальный, когда требуется рассчитать только часть дерева.
Отрисовка (Paint)
Во время этого этапа, браузер наполняет пиксели на экрана цветами в зависимости от контента.
Также, как и расчёт макета, отрисовка бывает глобальной и инкрементальной. Чтобы понять, какую часть viewport'а нужно перерисовать, браузер делить его ан прямоугольные блоки. Если изменения ограничены одним участком, то перерисуется только он.
Объединение слоев (compose)
Два предыдущих этапа используют CPU, поэтому они относительно медленные. Перерисовывать все элементы достаточно затратно, поэтому появился композитинг.
Композитинг - это разделение содержимого страницы на «слои», которые браузер будет перерисовывать. Эти слои друг от друга не зависят, из-за чего изменение элемента в одном слое не затрагивает элементы из других слоёв, и перерисовывать их становится не нужно. А итоговое изображение является объединением всех этих слоев в единой целое, причем данная операция выполняется уже на GPU, а она заточена на такие операции, так что выполнение происходит очень быстро.
Для выделения слоя используются следующие триггеры:
- СSS-свойства Perspective или 3D Transform;
- CSS Opacity animation;
- CSS Transform animation;
- Некоторые CSS-фильтры;
- Элементы <video /> с ускоренным видео-декодингом
- <canvas /> с 3D или ускоренным 2D контекстом
Заключение
Какие же итоги мы можем подвести? Браузер получает HTML, затем движок читает его и преобразовывает в DOM. После этого он пытается применить к DOM-дереву стили, образуя в следствии Render-дерево, с помощью которого браузер превращает древовидную структуру в пиксели и делит её на слои.
На картинке ниже кратко описан весь процесс загрузки страницы в браузере. Она также содержит JavaScript, так как в нынешних реалиях редко встречаются статичные страницы, и пользователь постоянно взаимодействует с ней при помощи JS.
Благодаря этой статье вы получили базовые знания, которые, надеюсь, помогут вам на пути к оптимизации клиентской скорости загрузки и производительности веб-приложения в целом.
Остались вопросы или есть, чем поделиться? Пиши в комментариях