← Назад к вопросам

Как работает рендеринг в Flutter?

2.7 Senior🔥 161 комментариев
#Архитектура Flutter

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как работает рендеринг в Flutter: Практический взгляд

За предыдущий вопрос я объяснил архитектуру. Здесь расскажу о механике: как именно Flutter преобразует виджеты в пиксели, этап за этапом, с акцентом на производительность и практику.

1. Инициализация рендеринга

Всё начинается при запуске приложения.

void main() {
  runApp(MyApp()); // Запускается рендеринг
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomePage(),
    );
  }
}

// Что происходит внутри:
// 1. Flutter создаёт RenderView (корневой RenderObject)
// 2. Вызывает build() MyApp
// 3. Конвертирует MaterialApp в RenderObject'ы
// 4. Запускает цикл VSYNC

2. VSYNC-управляемый цикл

Все рендеры синхронизированы с экраном через VSYNC.

┌────────────────────┐
│  VSYNC сигнал (60Hz)│ ← от GPU "готов новый кадр"
└─────────┬──────────┘
          ↓
    ┌─────────────┐
    │ Build Phase │
    └─────────────┘
          ↓
    ┌─────────────┐
    │Layout Phase │
    └─────────────┘
          ↓
    ┌─────────────┐
    │Paint Phase  │
    └─────────────┘
          ↓
    Показать на экране

Eсли что-то займет > 16.67мс (60FPS), кадр пропустится.

3. Build Phase в деталях

Этап, где создаётся дерево виджетов и RenderObject'ы.

// Flutter вызывает build() каждый раз при изменении
class HomePage extends StatefulWidget {
  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int counter = 0;

  @override
  Widget build(BuildContext context) {
    // ⚠️ Этот метод вызывается ДЕ ЧАСТО
    // При setState, при получении нового Widget parent'а, и т.д.
    return Scaffold(
      body: Center(
        child: Column(
          children: [
            Text('Count: $counter'), // Создаётся RenderParagraph
            ElevatedButton(
              onPressed: () => setState(() => counter++),
              child: Text('Increment'), // Ещё один RenderParagraph
            ),
          ],
        ),
      ),
    );
  }
}

// Build phase создаёт эту иерархию:
// RenderView
//   └─ RenderScaffold
//       ├─ RenderAppBar
//       └─ RenderCenter
//           └─ RenderFlex (Column)
//               ├─ RenderParagraph (Text)
//               └─ RenderConstrainedBox (Button)
//                   └─ RenderParagraph (Button text)

Оптимизация Build Phase:

// ❌ Создаёшь новые объекты каждый раз
WidgetToReuse buildComplexWidget() {
  return WidgetToReuse(
    child: ExpensiveDataProcess(),
  );
}

// ✅ Используй const
const complexWidget = WidgetToReuse(
  child: ExpensiveDataProcess(),
);

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return complexWidget; // Переиспользуется
  }
}

4. Layout Phase: Вычисление размеров

Каждый RenderObject вычисляет свой размер на основе constraints.

// Процесс Layout:
// 1. Root RenderView получает constraints (размер экрана)
// 2. Каждый RenderObject вычисляет свой размер
// 3. Размеры проходят вверх, constraints вниз

abstract class RenderBox extends RenderObject {
  // Constraints, полученные от родителя
  late BoxConstraints constraints;

  // Вычисленный размер
  late Size size;

  // Метод, который вычисляет размер
  void performLayout() {
    // Реализация в подклассах
    // Например, в RenderParagraph:
    // textPainter.layout(maxWidth: constraints.maxWidth);
    // size = constraints.constrain(textPainter.size);
  }
}

// Пример: Container с Text
Container(
  width: 300,
  color: Colors.blue,
  child: Text('Hello'),
)

// Layout процесс:
// 1. Container получает constraints (например, maxWidth: 500)
// 2. Container вычисляет: хочу 300 в ширину
// 3. Text получает constraints: maxWidth: 300
// 4. Text вычисляет: мне нужно 80 пикселей (для текста)
// 5. Результат:
//    - Container: 300x(высота зависит от Text)
//    - Text: 80x(его высота)

Дорогие операции Layout:

// ❌ Дорого: глубокое вложение
Column(
  children: [
    Container(
      child: Row(
        children: [
          Container(
            child: Column(
              // 4 уровня глубины = 4 Layout pass'а
            ),
          ),
        ],
      ),
    ),
  ],
)

// ✅ Лучше: плоская структура
Column(
  children: [
    MyComplexWidget(),
    MyComplexWidget(),
  ],
)

5. Paint Phase: Рисование

Каждый RenderObject рисует себя на Canvas.

class RenderContainer extends RenderBox {
  @override
  void paint(PaintingContext context, Offset offset) {
    // Paint по порядку:
    // 1. Фон
    if (decoration != null) {
      decoration.paint(
        context.canvas,
        Rect.fromLTWH(
          offset.dx,
          offset.dy,
          size.width,
          size.height,
        ),
      );
    }

    // 2. Дети
    if (child != null) {
      context.paintChild(child!, offset + childOffset);
    }

    // 3. Border
    if (decoration != null) {
      decoration.paintBorder(...);
    }
  }
}

// Пример на Canvas
canvas.drawRect(
  Rect.fromLTWH(10, 10, 100, 100),
  Paint()..color = Colors.blue,
);
// Это вызывает Skia C++ код, который говорит GPU:
// "Раскрась пиксели с (10,10) до (110,110) в синий"

Оптимизация Paint Phase:

// ❌ Перерисовывается весь граф при каждом изменении
class BadPerformance extends StatefulWidget {
  @override
  State<BadPerformance> createState() => _BadPerformanceState();
}

class _BadPerformanceState extends State<BadPerformance> {
  int counter = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $counter'), // Перерисовывается
        Text('Static text'),      // Перерисовывается зря!
        Text('Another text'),      // Перерисовывается зря!
      ],
    );
  }
}

// ✅ Лучше: изолируй перерисовку
class GoodPerformance extends StatefulWidget {
  @override
  State<GoodPerformance> createState() => _GoodPerformanceState();
}

class _GoodPerformanceState extends State<GoodPerformance> {
  int counter = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        CounterDisplay(counter: counter), // Перерисовывается
        const StaticContent(),             // НЕ перерисовывается
      ],
    );
  }
}

class StaticContent extends StatelessWidget {
  const StaticContent();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Static text'),
        Text('Another text'),
      ],
    );
  }
}

6. Display List

Результат Paint phase — это Display List (промежуточное представление).

Paint Phase создаёт Display List (в памяти):
[
  DrawRect(...),
  DrawText(...),
  DrawCircle(...),
  ...
]

Этот список передаётся Skia (C++), который:
1. Оптимизирует команды
2. Преобразует в GPU команды
3. Отправляет на GPU

7. Слои (Layers) и Compositing

Для эффектов (Opacity, Transform) используются слои.

// ❌ Без слоя: каждый раз пересчитывается
Transform(
  transform: Matrix4.identity()..setEntry(3, 2, 0.001),
  alignment: Alignment.center,
  child: MyWidget(),
)

// ✅ С слоем: более оптимально
RepaintBoundary(
  child: Transform(
    transform: Matrix4.identity()..setEntry(3, 2, 0.001),
    alignment: Alignment.center,
    child: MyWidget(),
  ),
)

// В Opacity это критично:
// ❌ Перестраивает дерево
Opacity(opacity: 0.5, child: HeavyWidget())

// ✅ Лучше: используй AnimatedOpacity
AnimatedOpacity(
  opacity: 0.5,
  duration: Duration(milliseconds: 300),
  child: HeavyWidget(),
)

8. Практический пример: Профилирование рендеринга

# Запусти в profile mode
flutter run --profile

# Включи Performance Overlay
# В коде или DevTools Timeline
import 'package:flutter/foundation.dart';

// Добавь в MaterialApp
showPerformanceOverlay: !kReleaseMode,

// Или явно отслеживай
SchedulerBinding.instance.addPersistentFrameCallback((_) {
  print('Frame took: ${DateTime.now()}')
});

9. Обход дерева

Render tree обходится в определённом порядке.

Pre-Order DFS (глубина сначала):
Root
  ├─ FirstChild
  │  ├─ GrandChild1
  │  └─ GrandChild2
  ├─ SecondChild
  └─ ThirdChild

Порядок вычисления Layout:
1. Root performLayout()
2. FirstChild performLayout()
3. GrandChild1 performLayout()
4. GrandChild2 performLayout()
5. SecondChild performLayout()
6. ThirdChild performLayout()

10. Batching изменений

Flutter группирует изменения в один кадр.

// ❌ Может быть 3 кадра
setState(() => a++);
setState(() => b++);
setState(() => c++);

// ✅ Один кадр
setState(() {
  a++;
  b++;
  c++;
});

11. Целая картина

APP START
  ↓
CreateRenderView
  ↓
VSYNC сигнал → Frame начинается
  ↓
BUILD: build() → RenderObject иерархия
  ↓
LAYOUT: performLayout() → размеры
  ↓
PAINT: paint() → Display List
  ↓
COMPOSIT: Skia + GPU
  ↓
Экран обновляется
  ↓
Следующий VSYNC → повторить

12. Ключевые моменты

Build часто, Paint редко:

  • Build может вызваться много раз (setState, didUpdateWidget)
  • Paint вызывается только если что-то визуально изменилось
  • Layout вызывается только если размеры изменились

Используй const:

  • const виджеты не перестраиваются
  • const конструкторы = нет новых объектов

Избегай дорогих операций:

  • В build(): нет вычислений
  • В layout(): нет IO операций
  • В paint(): максимум оптимизации

Вывод

Рендеринг в Flutter работает через:

  1. VSYNC синхронизация — 60 кадров в секунду
  2. Build — создание RenderObject иерархии
  3. Layout — вычисление размеров
  4. Paint — рисование на Canvas
  5. Composit — отправка на GPU

Чтобы приложение было быстрым:

  • Минимизируй Build work (используй const)
  • Профилируй Layout (избегай глубокого дерева)
  • Оптимизируй Paint (используй RepaintBoundary)
  • Понимай constraints и размеры

Flutter делает это всё автоматически, но понимание механики критично для высокопроизводительных приложений.