Убери лишние перестройки с помощью const, ключей и согласования, а потом измерь результат в DevTools.
Открыть этот урок в KodokonFlutter работает с тремя деревьями: виджеты (неизменяемые одноразовые конфигурации), Element-ы (живые экземпляры, которые хранят состояние) и render objects (вёрстка, отрисовка). Перестроить виджет дёшево; дорого обходятся вёрстка и каскадная отрисовка. Во время перестройки Element.updateChild применяет три правила по порядку: если новый виджет идентичен (тот же экземпляр) старому, всё поддерево пропускается; если Widget.canUpdate истинно (тот же runtimeType, тот же key), Element сохраняется и просто перенастраивается; иначе Element уничтожается и создаётся заново, вместе с состоянием. Ключевое слово const использует первое правило: константное выражение канонизируется компилятором, на каждой сборке возвращается тот же экземпляр, identical истинно - и вся ветка пропускается.
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');
}
}Для списков дочерних элементов согласование сопоставляет их сначала по позиции. Переставь местами элементы с состоянием без ключей - и каждый Element сохранит состояние прежнего жильца своей позиции: галочки окажутся не там, поля ввода перепутаются. Key меняет правило сопоставления: алгоритм находит Element с тем же ключом, даже если тот переехал внутри списка. Используй ValueKey с устойчивым бизнес-идентификатором. GlobalKey идёт дальше - он позволяет перенести целое поддерево к другому родителю, не потеряв его состояние, - но у него есть реальная цена (глобальный реестр, требование уникальности во всём дереве): береги его для настоящих задач вроде Form или смены родителя.
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)),
),
],
);
}
}Сначала измеряй, потом оптимизируй. В DevTools вкладка Performance показывает время каждого кадра для двух критичных потоков: UI (твой код на Dart: build, вёрстка) и raster (растеризация слоёв). Бюджет - около 16 мс при 60 Гц. Подтормаживания на стороне raster не лечатся оптимизацией сборок: они идут от стоимости отрисовки (тени, saveLayer, слишком большие изображения). Включи Track widget builds, чтобы увидеть, кто перестраивается, и debugRepaintRainbowEnabled, чтобы разглядеть перерисованные зоны - рамка меняет цвет при каждой перерисовке. RepaintBoundary выделяет поддерево в отдельный слой: постоянно крутящийся индикатор больше не пачкает соседей.
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(),
),
),
),
);
}
}