От кода до пикселей: как работает рендеринг. Часть 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.