Пайплайн (конвейер) рендеринга во Flutter — высокооптимизированный процесс, превращающий декларативный UI-код в пиксели на экране. Глубокое понимание пайплайна рендеринга способствует более разумному подходу к выбору структуры виджетов и управлению состояниями, что позволяет создавать более удобный в обслуживании и масштабируемый код.
Фазы пайплайна рендеринга
- Сборка (Build): создание и настройка виджетов на основе текущего состояния приложения.
- Макет (Layout): определение размера и положения каждого виджета (родительские ограничения определяют размеры дочерних).
- Отрисовка (Paint): отрисовка на canvas визуальных элементов (например, цвета, текста, рамок).
- Компоновка (Compositing): организация отрисованных виджетов по слоям.
- Растеризация (Rasterize): преобразование отрисованных слоев в пиксели, удобные для распознавания GPU, для последующего отображения.

Если вы откроете DevTools таким образом, то в середине увидите результат фазы сборки, а справа — результат фазы макета.
Flutter использует цикл событий для планирования обратных вызовов фреймов. Определив, что требуется новый фрейм — из-за взаимодействия с пользователем, анимации или изменения состояния, — Flutter вызывает методы вроде scheduleFrame на WidgetsBinding. Это запланирует обратный вызов, который будет добавлен в цикл событий Dart.
Как только обратный вызов фрейма выполнен, Flutter проходит через свой пайплайн рендеринга. На каждой фазе может быть запланирована дополнительная работа (например, микрозадачи или дальнейшие обратные вызовы фреймов), и все они координируются посредством цикла событий.
Если необходимо выполнить какую-то логику сразу после пайплайна рендеринга, можно использовать постфреймовые обратные вызовы. Наиболее распространенный случай — запуск контекстно-зависимой логики в методе initState:
@override
void initState() {
super.initState();
context; // <- здесь произойдет сбой, виджет еще не прикреплен
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
context; // <- работает
});
}
Что происходит в фазе сборки
Во время фазы сборки работа выполняется в основном в Widgets (слое виджетов) и Elements (слое элементов), а управляется в основном BuildOwner.
- Widgets:
Flutter вызывает метод build() каждого виджета, чтобы создать новое дерево виджетов, отражающее текущее состояние и конфигурацию UI. Когда выполняется build(), состояние блокируется, то есть вызов setState() запрещен.
- Elements:
Слой Element выступает в качестве посредника между неизменяемым слоем виджетов и изменяемыми объектами рендеринга. Во время фазы сборки Flutter использует элементы для согласования нового дерева виджетов (исходников?) с предыдущим, гарантируя обновление только необходимых частей UI и освобождая следующие фазы рендеринга от лишней работы, что делает сложность рендеринга сублинейной.
Хотя объекты рендеринга имеют решающее значение для макета и отрисовки UI, они создаются и обновляются в последующих фазах (макета и отрисовки), а не непосредственно во время фазы сборки.
Сублинейная сложность
Когда говорится, что рендеринг во Flutter является сублинейным, имеется в виду, что вычислительные усилия, необходимые для рендеринга UI, растут медленнее, чем линейно, с увеличением количества виджетов или элементов. Другими словами, если удваивается количество виджетов, затраты времени или работы, необходимые для рендеринга фрейма, не обязательно удваиваются.
Как Flutter добивается этого?
Сравнение деревьев виджетов
Когда состояние меняется, Flutter запускает процесс сравнения нового дерева виджетов с предыдущим (Widget Tree Diffing) и обновляет только те части, которые изменились. Однако ваша задача — уведомить об изменении состояния и обеспечить оптимальную область обновления виджета, вызвав setState в правильном контексте (или корректно используя любое другое управление состоянием). Обновление области видимости принимает в качестве аргумента Element, который как раз и инкапсулирует BuildContext.
Даже если процесс сравнения виджетов оказался эффективным и удалось минимизировать лишний рендеринг, запуск всех методов сборки все равно требует некоторой вычислительной мощности.
Рендеринг на основе слоев
Flutter рендерит UI-элементы по слоям, которые затем компонуются GPU. Если меняется лишь небольшая часть UI, то обновлять нужно только соответствующий(е) слой(и). Ваша задача состоит в том, чтобы оптимально изолировать различные части UI по компонуемым слоям.
Кэширование и ленивые обновления
Многие виджеты неизменяемы и могут кэшироваться. Если виджет или его поддерево не изменились, Flutter повторно использует существующую информацию о рендеринге. Такая стратегия кэширования позволяет избежать избыточных вычислений и операций отрисовки. Именно в этот момент использование виджетов const выполняет свою задачу.
4. Виджеты
Теперь разберем, что происходит в слое виджетов.
Вызов метода build
Когда виджету необходимо обновиться (из-за изменения состояния, изменения родителя или других триггеров), вызывается его метод build(). Этот метод возвращает новую конфигурацию виджетов, которая описывает UI в данный момент.
Создание дерева неизменяемых виджетов
Виджеты во Flutter неизменяемы. Новое дерево виджетов, созданное методом build(), служит образцом того, как должен выглядеть UI. Это дерево является легковесным и содержит только данные конфигурации, а не реальные UI-элементы.
Использование ключей
Виджеты могут использовать ключи для сохранения своей идентичности при перестройке. Это особенно важно для списков или динамических интерфейсов, поскольку ключи помогают Flutter сопоставлять новые экземпляры виджетов с соответствующими элементами в существующем дереве.
После построения нового дерева виджетов Flutter передает его слою элементов. Подробнее прочитать о том, как Flutter преобразует виджеты в элементы, можно в его документации.
Элементы
Понимать, что происходит в слое Element, необходимо для того, чтобы сохранить дерево Element оптимальным и избежать избыточного выделения.
- В этой фазе рендеринга элементы создаются, монтируются и обновляются. Фреймворк вызывает
mount, чтобы добавить только что созданный элемент в деревоElement. Эта операция называетсяBuildContext.mounted. ЕслиElementне был добавлен в деревоElement(или удален из него),BuildContextбудет недействительным для использования.
Elementпосле того, как будет размонтирован, не может быть смонтирован обратно в дерево. Это односторонний переход. Element считается «недействующим» («defunct») и не будет включен в дерево в будущем.
- Хотя сам размонтированный
Elementне может быть использован повторно, если у него былGlobalKey, этот ключ может быть повторно использован в новомElement. Этот новыйElementбудет полностью отделен от размонтированного.
- Чтобы запланировать (пере)сборку, Flutter помечает обновленные элементы как грязные, а затем в фазе сборки итерирует по ним и вызывает их метод
performRebuild(). Этот метод, в свою очередь, вызывает методbuild()элемента, чтобы сгенерировать новое поддерево виджетов, которое, в свою очередь, вызывает методbuild()виджета и передает себя в качествеBuildContext.
Элементы могут размонтироваться в нескольких сценариях. Рассмотрим основные сценарии размонтирования элементов и затем сравним их со сценариями деактивирования элементов.
Завершение очистки фрейма
В конце фрейма процесс очистки Flutter выполняет итерации по дереву элементов, находит элементы, которые больше не прикреплены (т. е. осиротели), и вызывает их метод unmount(), чтобы отсоединить их и избавиться от всех связанных с ними ресурсов. Это может произойти из-за удаления родителя или использования условного виджета.
Widget build(BuildContext context) {
return isVisible
? MyWidget() // Когда isVisible становится false:
// 1. MyWidget's Element деактивируется
// 2. Позже размонтируется в конце фрейма
: SizedBox(); // Создается новый Element
}
Навигация
Когда маршрут полностью раскрывается и удаляется, все дерево элементов этого маршрута размонтируется после завершения анимации.
Navigator.pop(context);
Теперь посмотрим, когда элемент деактивируется, а затем снова активируется.
Виджет Offstage
Элемент остается активным даже при скрытии, в отличие от условного рендеринга, который деактивирует его.
Offstage(
offstage: isHidden,
child: MyWidget(),
);
Перемещение виджета между родителями с помощью GlobalKey
BuildOwner содержит все глобальные ключи, связанные с соответствующими элементами. При раздувании виджетов он забирает деактивированный элемент, если есть глобальный ключ, в противном случае создает новый элемент.
Переупорядочивание элементов списка
Когда вы переупорядочиваете элементы в ListView, алгоритм согласования Flutter с помощью уникальных ключей, назначенных каждому виджету, повторно использует связанные с ними Elements.
В фазе сборки Flutter сравнивает новый список виджетов с предыдущим, проверяя ключ каждого виджета:
- если виджет с таким же ключом существует в предыдущем списке, Flutter повторно использует его
Element;
- если необходимо, существующий
Elementобновляется с новой конфигурацией виджета, но его состояние и другие связанные с ним данные остаются нетронутыми.
Однако не стоит ожидать повышения производительности, просто добавляя ключи к элементам списка, если вы не собираетесь их упорядочивать.
AnimatedSwitcher с тем же дочерним типом
AnimatedSwitcher(
duration: Duration(milliseconds: 500),
child: showFirst
? Container(
key: childKey, // Element деактивируется во время анимации
color: Colors.blue,
height: 100,
width: 100,
)
: Container(
key: childKey, // и повторно активируется в новом положении
color: Colors.red,
height: 100,
width: 100,
),
),
Сравнение деактивированных и размонтированных элементов
Различия:
- Деактивированные
Elementsсохраняют отношения унаследованных виджетов (InheritedWidgets).
- Деактивация временная (длится до конца фрейма).
- Размонтированные
Elementsочищают все отношения.
- Размонтирование является постоянным.
Читайте также:
- Организация “глобальных” провайдеров во Flutter Riverpod с помощью миксинов
- Рецензирование кода Flutter: лучшие практики
- Flutter зовет: 5 проектов за выходные
Читайте нас в Telegram, VK и Дзен
Перевод статьи Roman Ismagilov: Understanding Flutter rendering pipeline: Build phase





