Kodokon kodokon.com

Производительность: const, перестройки, ключи и DevTools

Убери лишние перестройки с помощью const, ключей и согласования, а потом измерь результат в DevTools.

10 мин · 3 вопросов

Открыть этот урок в Kodokon

Flutter работает с тремя деревьями: виджеты (неизменяемые одноразовые конфигурации), Element-ы (живые экземпляры, которые хранят состояние) и render objects (вёрстка, отрисовка). Перестроить виджет дёшево; дорого обходятся вёрстка и каскадная отрисовка. Во время перестройки Element.updateChild применяет три правила по порядку: если новый виджет идентичен (тот же экземпляр) старому, всё поддерево пропускается; если Widget.canUpdate истинно (тот же runtimeType, тот же key), Element сохраняется и просто перенастраивается; иначе Element уничтожается и создаётся заново, вместе с состоянием. Ключевое слово const использует первое правило: константное выражение канонизируется компилятором, на каждой сборке возвращается тот же экземпляр, identical истинно - и вся ветка пропускается.

DART
import 'package:flutter/material.dart';

class CounterScreen extends StatefulWidget {
  const CounterScreen({super.key});

  @override
  State<CounterScreen> createState() =>
      _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const _Header(),
        Text('Total: $_count'),
        FilledButton(
          onPressed: () => setState(() => _count++),
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

class _Header extends StatelessWidget {
  const _Header();

  @override
  Widget build(BuildContext context) {
    debugPrint('build _Header');
    return const Text('Counter');
  }
}
Лог появляется всего один раз: const пропускает поддерево.

Для списков дочерних элементов согласование сопоставляет их сначала по позиции. Переставь местами элементы с состоянием без ключей - и каждый Element сохранит состояние прежнего жильца своей позиции: галочки окажутся не там, поля ввода перепутаются. Key меняет правило сопоставления: алгоритм находит Element с тем же ключом, даже если тот переехал внутри списка. Используй ValueKey с устойчивым бизнес-идентификатором. GlobalKey идёт дальше - он позволяет перенести целое поддерево к другому родителю, не потеряв его состояние, - но у него есть реальная цена (глобальный реестр, требование уникальности во всём дереве): береги его для настоящих задач вроде Form или смены родителя.

DART
import 'package:flutter/material.dart';

class TodoList extends StatelessWidget {
  const TodoList({super.key, required this.items});

  final List<String> items;

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        for (final item in items)
          Dismissible(
            key: ValueKey(item),
            onDismissed: (direction) {},
            child: ListTile(title: Text(item)),
          ),
      ],
    );
  }
}
Без устойчивого ключа Dismissible выбросит ошибку при первом же удалении.

Сначала измеряй, потом оптимизируй. В DevTools вкладка Performance показывает время каждого кадра для двух критичных потоков: UI (твой код на Dart: build, вёрстка) и raster (растеризация слоёв). Бюджет - около 16 мс при 60 Гц. Подтормаживания на стороне raster не лечатся оптимизацией сборок: они идут от стоимости отрисовки (тени, saveLayer, слишком большие изображения). Включи Track widget builds, чтобы увидеть, кто перестраивается, и debugRepaintRainbowEnabled, чтобы разглядеть перерисованные зоны - рамка меняет цвет при каждой перерисовке. RepaintBoundary выделяет поддерево в отдельный слой: постоянно крутящийся индикатор больше не пачкает соседей.

DART
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() {
  debugRepaintRainbowEnabled = true;
  runApp(const DemoApp());
}

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      showPerformanceOverlay: true,
      home: Scaffold(
        body: Center(
          child: RepaintBoundary(
            child: CircularProgressIndicator(),
          ),
        ),
      ),
    );
  }
}
Оверлей кадров плюс радуга перерисовок: диагностический дуэт.

Проверка знаний

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

  1. Почему const-виджет игнорируется, когда его родитель перестраивается?
    • Экземпляр канонический: identical истинно, и Element.updateChild пропускает всё поддерево
    • Компилятор убирает виджет из дерева в release
    • У const-виджетов нет метода build
  2. Ты переставляешь список виджетов с состоянием без ключей. Что произойдёт?
    • Элементы дерева Element сопоставляются по позиции: каждый наследует состояние прежнего жильца своей позиции
    • Flutter выбросит исключение в режиме debug
    • Состояние автоматически следует за каждым перемещённым виджетом
    • Список откажется перестраиваться
  3. Что измеряют два графика оверлея производительности?
    • Время каждого кадра в потоке UI (код на Dart) и в потоке raster (отрисовка слоёв)
    • Выделенную память и количество виджетов
    • Время сборки и время сборки мусора