February 20, 2023

Как браузер рисует страницы

При загрузке сайта, браузер выполняет множество операций, которые так или иначе влияют на отрисовку страницы. Эти операции в целом можно поделить на несколько стадий.

  1. Загрузка основного ресурса
  2. Получение внешних ресурсов
  3. Выполнение JavaScript
  4. Построение Render-дерева
  5. Расчёт макета (Layout)
  6. Отрисовка документа (Paint)
  7. Объединение слоёв (Composite)

Загрузка основного ресурса

Под основным ресурсом подразумевается точка входа в веб-приложение. В большинстве случаев это обычный index.html файл.

После загрузки изначального ресурса, браузер начинает парсинг HTML-файла, выделяя "токены", которые представляют собой начало тега, конец тега и его содержание.

Из всего анализа документа, браузер строит так называемое DOM-дерево.

Парсинг HTML в DOM

Получение внешних ресурсов JavaScript

Помимо HTML ресурсов, веб-приложение так-же может задействовать внешние ресурсы, например такие, как CSS или JS файлы, картинки, а так-же аудио-файлы.

Загрузка JavaScript-файлов

Как только браузер доходит до инициализации JavaScript-файлов, он блокирует парсинг HTML. После чего начинается скачивание и выполнение скриптов.

Загрузка скриптов JS во время парсинга HTML файла

Однако в наше время загрузка скриптов выглядит иначе и браузер, доходя до первого скрипта, начинает искать все остальные скрипты и скачивать их одновременно. Важно подметить, что хоть скрипты загружаются одновременно, запуск всё равно идёт последовательно.

Одновременная загрузка всех скриптов JS

Почему же загрузка скриптов JS требует остановки парсинга? Дело в document.write, который работает только на этапе парсинга и модифицирует исходный документ в том месте, где был вызван скрипт и заблокирован парсер.

Атрибуты async и defer

Мы можем влиять на порядок загрузки скриптов посредством использования атрибутов async и defer. В случае с async, скрипт будет запрошен сразу и будет блокировать парсинг только на этапе выполнения скрипта. Соответственно остальные асинхронная загрузка скрипта не даёт гарантии, что он загрузится в правильном порядке относительно остальных скриптов.

Загрузка скрипта JS с атрибутом async

При использовании defer, скрипт выполнится сразу после парсинга страницы, не блокируя его. При чём скрипт будет сохранять порядок выполнения для всех остальных defer скриптов в вызываемой последовательности.

Важные особенности:

  • Каждый предыдущий defer-скрипт будет блокировать следующий.
  • defer откладывает событие DOMContentLoaded, в отличии от async
Загрузка скрипта JS с атрибутом defer

Получение внешних ресурсов CSS

В отличии от получения JavaScript ресурсов, стили не блокируют парсинг HTML-файла, однако блокируют отрисовку страницы.

Рекомендуется инициализировать CSS-файлы до скриптов, чтобы браузер грузил стили в первую очередь. В противном случае, браузер начнет скачивать стили только после того, как скачает и выполнит JS-код, что соответственно отложит отрисовку.

Загрузка CSS и JS ресурсов во время парсинга HTML

Создание CSSOM

После загрузки стилей, браузер строит из полученных ресурсов так называемый CSSOM (Cascade Style Sheets Object Model). Это что-то похожее на DOM, только для стилей, где для каждого стиля есть соответствующий узел.

  • CSS - блокирующий обработку ресурс, соответственно Render Tree не будет построено без полного построения CSSOM.
  • Парсинг CSS, в отличии от HTML, не может работать по частям в силу своих особенностей. Ведь в HTML-документе мы можем переопределять и изменять стили, написанные выше. Поэтому, чтобы перейти к следующему шагу, необходимо полностью преобразовать CSS.
Преобразования CSS кода в CSSOM

Выполнение JS кода

После переломного момента, когда HTML загружен, преобразован в DOM, а JavaScript спаршен, генерируется так называемое событие DOMContentLoaded.

  • Остальные ресурсы, такие как картинки, или стили, могут быть ещё не загружены.

Для любого сценария, задействующего модель DOM, рекомендуется сначала дождаться этого события.

document.addEventListener('DOMContentLoaded', () => {
  // Код, задействующий модель DOM
});

После завершения загрузки HTML со всеми внешними ресурсами (картинки, стили, асинхронный JS и т.д.), срабатывает событие браузера, под названием load.

  • Важно то, что событие load наступает у объекта window.
window.addEventListener('load', () => {
  // Страница полностью загружена
});
Особенности срабатывания событий DOMContentLoaded и window.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 />

Объединение DOM с CSSOM

Расчёт макета (Layout)

После создания Render-дерева, браузер начинает позиционировать элементы на странице. Для этого он раcсчитывает размеры и положение каждого элемента рекурсивно. Расчёт начинается от корневого элемента Render-дерева, размер которого равен размеру viewport'а.

Layout также может быть глобальным, когда требуется расcчитать положение render объектов всего дерева, и инкрементальный, когда требуется рассчитать только часть дерева.

Расчёт макета относительно HTML и CSS кода

Отрисовка (Paint)

Во время этого этапа, браузер наполняет пиксели на экрана цветами в зависимости от контента.

Также, как и расчёт макета, отрисовка бывает глобальной и инкрементальной. Чтобы понять, какую часть viewport'а нужно перерисовать, браузер делить его ан прямоугольные блоки. Если изменения ограничены одним участком, то перерисуется только он.

Объединение слоев (compose)

Два предыдущих этапа используют CPU, поэтому они относительно медленные. Перерисовывать все элементы достаточно затратно, поэтому появился композитинг.

Композитинг - это разделение содержимого страницы на «слои», которые браузер будет перерисовывать. Эти слои друг от друга не зависят, из-за чего изменение элемента в одном слое не затрагивает элементы из других слоёв, и перерисовывать их становится не нужно. А итоговое изображение является объединением всех этих слоев в единой целое, причем данная операция выполняется уже на GPU, а она заточена на такие операции, так что выполнение происходит очень быстро.

Для выделения слоя используются следующие триггеры:

  • СSS-свойства Perspective или 3D Transform;
  • CSS Opacity animation;
  • CSS Transform animation;
  • Некоторые CSS-фильтры;
  • Элементы <video /> с ускоренным видео-декодингом
  • <canvas /> с 3D или ускоренным 2D контекстом
Представление слоёв страницы https://ya.ru/

Заключение

Какие же итоги мы можем подвести? Браузер получает HTML, затем движок читает его и преобразовывает в DOM. После этого он пытается применить к DOM-дереву стили, образуя в следствии Render-дерево, с помощью которого браузер превращает древовидную структуру в пиксели и делит её на слои.

На картинке ниже кратко описан весь процесс загрузки страницы в браузере. Она также содержит JavaScript, так как в нынешних реалиях редко встречаются статичные страницы, и пользователь постоянно взаимодействует с ней при помощи JS.

Процесс загрузки страницы

Благодаря этой статье вы получили базовые знания, которые, надеюсь, помогут вам на пути к оптимизации клиентской скорости загрузки и производительности веб-приложения в целом.

Остались вопросы или есть, чем поделиться? Пиши в комментариях