От кода до пикселей: как работает рендеринг. Часть 3. Стадии основного потока.

От кода до пикселей: как работает рендеринг. Часть 3. Стадии основного потока.

Anastasia Kotova

Предыдущие части

От кода до пикселей: как работает рендеринг. Часть 1. Браузерные движки.

От кода до пикселей: как работает рендеринг. Часть 2. Конвейер рендеринга.


В предыдущей части мы взглянули на конвейер рендеринга в Chromium целиком. Он выглядит так:

1. Парсинг
   → парсинг HTML и CSS
   → построение DOM и таблиц стилей
2. Animate
   → обновление анимаций и временных эффектов
3. Style
   → применение CSS и вычисление итоговых стилей
4. Layout
   → расчёт размеров и позиций элементов
5. Pre-paint
   → подготовка к отрисовке, обновление визуальных состояний
6. Scroll
   → обработка прокрутки и смещения контента
7. Paint
   → формирование команд отрисовки
8. Commit
   → передача данных с main thread на compositor
9. Layerize
   → разбиение сцены на слои
10. Raster / Decode / Paint Worklets
   → растрирование, декодирование изображений, генерация текстур
11. Activate
   → сборка кадра для отображения
12. Aggregate
   → объединение кадров от разных источников
13. Draw
  → отрисовка через GPU

Пришло время погружаться глубже. В этой статье мы разберём этапы, которые выполняются в основном потоке — от парсинга до стадии commit.

Парсинг

Когда HTML-документ начинает загружаться, Blink запускает потоковый парсинг входного байтового потока. Парсер работает инкрементально: он обрабатывает документ по мере поступления данных из сети, что позволяет начинать рендеринг ещё до полной загрузки страницы.

Входной поток разбивается на токены в соответствии со спецификацией HTML5. На их основе Chromium строит DOM-дерево, применяя правила построения документа, включая обработку некорректного HTML.

Параллельно с этим браузер парсит CSS и формирует на его основе CSSOM — CSS Object Model. Это дерево правил, оптимизированное для эффективного сопоставления стилей с элементами DOM.

Важная особенность парсера Chromium — спекулятивный preload scanner, работающий параллельно основному парсеру. Он просматривает HTML «на опережение», находит ссылки на внешние ресурсы (скрипты, стили, изображения) и инициирует их загрузку заранее, что заметно ускоряет общее время загрузки страницы.

Animate

После парсинга и построения DOM, когда приходит время обновить отображение, Chromium запускает пайплайн формирования кадра. Первый значимый этап в нём — фаза animate.

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

В animate вызываются функции, зарегистрированные через requestAnimationFrame, так как их результат должен быть готов до начала layout. Обычно это изменения DOM или стилей.

Также в этой фазе Chromium вычисляет текущее состояние всех активных CSS-анимаций (@keyframes) и transition. Для этого он берёт текущий timestamp, учитывает duration, delay, easing-функции и timeline, после чего интерполирует значения свойств между ключевыми кадрами. В результате получаются конкретные вычисленные значения стилей для текущего кадра.

После завершения этих вычислений элементы помечаются dirty-флагами. Уже на следующей фазе — style — браузер пересчитывает стили только для изменённых узлов.

Фаза animate размещена перед style и layout не случайно: это позволяет собрать все изменения от анимаций заранее и обработать их за один проход, избегая лишних пересчётов.

Style

Фаза style отвечает за пересчёт стилей и всегда выполняется в основном потоке. Её задача — собрать все изменения, накопленные на предыдущих этапах (парсинг, выполнение скриптов, анимации), и превратить их в итоговые вычисленные стили элементов.

Chromium не пересчитывает стили для всего дерева целиком. Он использует систему dirty-флагов: когда что-то меняется — класс, inline-стиль, DOM-структура или результат анимации — элемент и его предки помечаются как «грязные». Пересчёт выполняется только для этих ветвей.

Далее для каждого такого элемента собираются подходящие CSS-правила из CSSOM с учётом каскада, специфичности и наследования. После этого формируется итоговый computed style.

Важно, что селекторы индексируются по последнему простому селектору — тегу, классу или id. Поэтому сопоставление идёт справа налево, что на практике оказывается наиболее эффективным.

В конце этой фазы все «грязные» элементы имеют актуальные computed styles, и пайплайн переходит к layout.

Layout

На этапе layout браузер превращает DOM и вычисленные стили в конкретные размеры и позиции элементов на экране.

Для этого используются разные алгоритмы разметки: обычный flow layout, flexbox и grid. Каждый из них работает по принципу взаимодействия родителей и потомков: родитель передаёт дочернему элементу ограничения (например, доступную ширину), а тот, опираясь на них и свои стили, вычисляет размеры.

Блочный layout обычно укладывается в один проход, а flex и grid требуют нескольких этапов. Сначала вычисляются естественные размеры элементов, затем выполняется проход размещения. Раньше при глубокой вложенности таких контейнеров сложность могла расти экспоненциально. В современной архитектуре Chromium эта проблема решена с помощью явного кэширования промежуточных результатов.

Итогом фазы layout становится неизменяемое дерево фрагментов (fragment tree), в котором каждый фрагмент хранит размеры и позицию элемента. После создания фрагменты не модифицируются, а также в дереве нет ссылок «вверх» и передачи данных от родителя к потомку. Благодаря этому при следующем обновлении можно переиспользовать большую часть дерева, пересчитывая только изменённые ветви.

Текстовый и инлайн-контент хранится отдельно — в виде плоского списка. Это ускоряет обход и снижает расход памяти, что особенно важно для рендеринга текста.

Pre-paint

Фаза pre-paint следует сразу после layout. На этом этапе выполняется обход дерева фрагментов с двумя основными целями.

Первая — paint invalidation. На предыдущих фазах элементы помечаются как изменённые, и именно pre-paint определяет, какие области экрана нуждаются в перерисовке.

Вторая цель — построение property trees.

Property trees представляют собой четыре отдельных дерева: transform, clip, effect и scroll. В них присутствуют только те элементы, которые реально создают соответствующие эффекты. Структура этих деревьев отражает не сам DOM, а отношения «контейнер → потомок» между визуально значимыми узлами.

  • Transform tree хранит всё связанное с позицией и движением элемента: смещение, CSS-трансформации, perspective и scroll translation.
  • Clip tree описывает все виды обрезки: clip-path, overflow clip, обрезку по border-radius.
  • Effect tree хранит визуальные эффекты: opacity, фильтры, маски.
  • Scroll tree отвечает за цепочки прокрутки

Между деревьями существуют перекрёстные ссылки, указывающие на относительный порядок применения эффектов разных типов. Каждый DOM-элемент имеет свой property tree state — набор из четырёх ссылок на соответствующие узлы. Именно он позволяет точно определить положение элемента и способ его отрисовки.

Scroll

Фаза scroll — одна из немногих, которая может полностью выполняться в композитном потоке, минуя основной. Её главная цель: обеспечить плавный скроллинг даже тогда, когда основой поток занят тяжёлым JavaScript или другими задачами.

Обновление scroll offset сводится к изменению одного узла в transform tree, а все потомки автоматически сдвигаются вместе с ним. Именно поэтому скроллинг дёшев — ни layout, ни pre-paint, ни paint не задействованы.

В некоторых случаях скролл не может быть обработан на композиторе — например, при наличии синхронных обработчиков touch-событий. Тогда событие передаётся в основной поток. Такие ситуации называются slow scroll, в отличие от fast scroll в композитном потоке.

Paint

Фаза paint следует сразу после scroll. Её задача — превратить дерево фрагментов и property trees в плоский список команд рисования, который позже можно отрастеризовать в текстуры на GPU.

Этот список называется display list, а его атомарная единица — display item. Каждый display item представляет собой одну конкретную операцию: отрисовку фона, текста, границ и других визуальных элементов.

Каждый display item создаётся определённым объектом из дерева фрагментов. Кроме того, при создании ему присваивается текущий property tree state — та самая четвёрка (transform, clip, effect, scroll), которую мы рассматривали ранее. Именно она определяет, в каком пространстве координат и с каким набором эффектов нужно растеризовать данный элемент.

После формирования display list он разбивается на paint chunks — группы соседних display items с одинаковым property tree state. Границы между чанками проходят там, где состояние меняется. Например, если одно поддерево прокручивается, а соседнее — нет, они окажутся в разных чанках.

Каждый paint chunk является потенциальным кандидатом для отдельного composited layer на следующей фазе — layerize. Чанки служат основной единицей гранулярности для raster invalidation: при изменениях система сравнивает новые чанки со старыми и определяет, какие области нужно перерастеризовать.

Commit

Commit — это фаза синхронизации между основным и композитным потоками.

До этого момента вся работа — style, layout, pre-paint и paint — выполняется на основном потоке и оперирует такими структурами данных, как дерево фрагментов, property trees и display list. Композитный поток, в свою очередь, работает со своей копией этих данных. Commit — единственная точка, где эти два мира синхронизируются: данные из основного потока атомарно копируются в композитный. Для этого основной поток блокируется — он останавливается и ждёт завершения копирования. Сам commit — относительно дешёвая операция и обычно занимает несколько миллисекунд. Однако его нельзя прервать посередине, иначе композитный поток получил бы несогласованное состояние.

Все скопированные данные попадают не сразу в «живое» дерево, а в так называемый pending tree — промежуточную структуру. Pending tree существует для обеспечения атомарности визуальных изменений. Если за один кадр произошло несколько мутаций — сдвиг элемента, изменение фона, отрисовка на canvas — пользователь должен увидеть их одновременно, а не по очереди.

После завершения commit основной поток разблокируется и может начинать подготовку следующего кадра.

Ещё немного про animate

Фаза animate формально располагается в начале рендеринг-пайплайна — перед style — но по своей природе она отличается от остальных фаз. Она может выполняться как на основном потоке, так и целиком на композитном, и именно это разделение играет ключевую роль для производительности.

В Chromium существует два типа анимаций.

Первый тип — анимации основного потока. Они мутируют свойства, влияющие на layout, например width, height или left. Такие анимации работают через стандартный цикл: на каждом кадре движок вычисляет текущее значение, помечает элемент как «грязный», после чего запускается полный пайплайн: style → layout → pre-paint → paint.

Второй тип — анимации, которые могут полностью обходить основной поток. Кандидатами на ускорение являются анимации, затрагивающие свойства transform, opacity, filter и backdrop-filter. Эти свойства представлены в property trees в виде узлов transform и effect и могут мутироваться в композитном потоке без пересчёта layout.

Report Page