Публикация была переведена автоматически. Исходный язык: Русский
Как показывает практика — rebuild, repaint и почему setState() обычно не главная проблема...
У Flutter есть странная репутация. Одни считают его магией, где всё “летает из коробки”. Другие — фреймворком, который начинает лагать, как только экран становится сложнее счетчика и списка карточек. Правда, как обычно, скучнее и полезнее: Flutter чаще всего тормозит не потому, что вы “слишком много вызываете setState()”, а потому, что неправильно понимаете, что именно в этот момент происходит.
И если этот момент понять, внезапно становится легче писать и быстрые интерфейсы, и более предсказуемый код.
Главная ошибка: путать rebuild и repaint
Когда разработчик говорит: “Я вызвал setState(), и у меня перерисовался весь экран”, обычно он смешивает сразу несколько разных стадий.
Во Flutter есть как минимум четыре важных шага:
- build — framework пересобирает дерево виджетов;
- layout — вычисляет размеры и позиции;
- paint — рисует содержимое;
- compositing / rasterization — готовит и отправляет всё на GPU.
И вот ключевая мысль: rebuild не равен repaint, а repaint не всегда означает тяжёлую работу на GPU.
Widget во Flutter — лёгкая декларация. Само по себе пересоздание дерева виджетов обычно не является катастрофой. Настоящие проблемы начинаются, когда за rebuild тянется дорогой layout, тяжёлый paint, лишние эффекты, большие списки, изображения, шейдеры, blur, тени или просто слишком широкая область обновления.
То есть вопрос не “сколько раз вызвали setState()”, а какой объём работы потянулся за этим вызовом.
Почему setState() демонизируют зря
Вот типичный пример:
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Column(
children: [
const HeavyHeader(),
Text('$count'),
ElevatedButton(
onPressed: () {
setState(() {
count++;
});
},
child: const Text('Increment'),
),
],
),
);
}
}Многие смотрят на это и думают: “Плохо, потому что пересобирается весь Column”.
Но сам rebuild этого поддерева почти наверняка дешёвый. Проблема была бы не в setState(), а в том, что внутри HeavyHeader может оказаться что-то реально тяжёлое: анимации, сложный кастомный paint, картинки, shader mask, blur или лишние layout-проходы.
То есть лечить тут надо не setState(), а границы обновления и реальную стоимость UI.
Где Flutter начинает реально страдать
Вот типичные источники jank, которые встречаются намного чаще, чем “слишком много rebuild”:
1. Тяжёлая работа внутри build()
build() должен быть дешёвым и предсказуемым. Если внутри вы:
- фильтруете большой список,
- сортируете данные,
- парсите JSON,
- собираете сложную карту UI-моделей,
- дергаете синхронную бизнес-логику,
то проблема не в Flutter, а в том, что вы превратили декларацию интерфейса в вычислительный пайплайн.
Плохой пример:
@override
Widget build(BuildContext context) {
final visibleItems = items
.where((e) => e.isActive)
.toList()
..sort((a, b) => a.name.compareTo(b.name));
return ListView.builder(
itemCount: visibleItems.length,
itemBuilder: (_, index) => ItemTile(item: visibleItems[index]),
);
}Если items большой и экран пересобирается часто, вы будете платить за это снова и снова.
Лучше вынести вычисления:
- в состояние,
- в selector,
- в memoization,
- в отдельный слой данных.
2. Слишком широкая область состояния
Когда локальное изменение живёт слишком высоко в дереве, вы заставляете framework заново проходить большой кусок UI без необходимости.
Плохой паттерн — держать всё состояние страницы в одном StatefulWidget, где любое изменение бьёт по всему экрану.
Лучше дробить экран на более мелкие участки и поднимать состояние ровно настолько, насколько нужно, а не “на всякий случай”.
3. Непонимание, когда нужен RepaintBoundary
RepaintBoundary не ускоряет всё подряд. Он полезен, когда у вас есть часть UI, которую не нужно заново перерисовывать, даже если рядом что-то активно меняется.
Например:
- тяжёлый статичный header,
- сложный кастомный график,
- карточка с дорогой отрисовкой,
- вложенная анимация, которую лучше изолировать.
Но если бездумно обложить весь экран RepaintBoundary, можно получить обратный эффект: больше слоёв, больше памяти, меньше пользы.
4. Неправильная работа со списками
Очень много проблем во Flutter приходят не из state management, а из списков.
Типичные ошибки:
- строить весь список сразу вместо ListView.builder;
- использовать shrinkWrap: true там, где он заставляет список измерять себя полностью;
- класть тяжёлые виджеты внутрь элементов списка;
- терять identity элементов из-за неудачной работы с Key.
И отдельно: Key — это не “ускоритель Flutter”. Key нужен прежде всего для сохранения идентичности элементов, а не для магической оптимизации.
Что делать на практике
Вот несколько правил, которые действительно помогают.
1. Делайте дешёвый build()
build() должен описывать UI, а не выполнять тяжёлую работу.
Хороший ментальный вопрос: если build() вызовется 60 раз в секунду, этот код всё ещё безопасен?
Если ответ “нет”, значит туда попало лишнее.
2. Локализуйте изменения
Если меняется только счётчик — не нужно пересобирать весь экран. Если меняется только кнопка — не нужно трогать header. Если обновляется один item в списке — не нужно гонять всю страницу.
Например, вместо одного большого StatefulWidget часто лучше выделить маленький виджет с собственной зоной ответственности:
class LikeButton extends StatefulWidget {
const LikeButton({super.key});
@override
State<LikeButton> createState() => _LikeButtonState();
}
class _LikeButtonState extends State<LikeButton> {
bool liked = false;
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(liked ? Icons.favorite : Icons.favorite_border),
onPressed: () {
setState(() {
liked = !liked;
});
},
);
}
}Такой код не “современнее” сам по себе, но он лучше держит границы обновления.
3. Используйте const везде, где это честно возможно
const — не волшебная таблетка, но очень полезный сигнал для framework: этот объект не меняется, его можно не пересоздавать заново.
const Text('Profile')
const SizedBox(height: 16)
const Icon(Icons.settings)Это не решит проблемы плохой архитектуры, но уменьшит лишний шум в дереве.
4. Разделяйте состояние и отрисовку
Во Flutter легко скатиться в код, где UI, бизнес-логика, фильтрация данных и анимации смешаны в одном build().
Чем чище разделены:
- состояние,
- производные данные,
- эффекты,
- UI,
тем легче контролировать производительность.
И не так важно, используете вы Provider, Riverpod, Bloc, Cubit или что-то ещё. Большинство проблем производительности приходят не из выбранной библиотеки, а из того, насколько точно вы обновляете нужный участок дерева.
5. Профилируйте, а не угадывайте
Это, наверное, самый полезный совет.
Flutter DevTools показывает намного больше, чем кажется:
- frame rendering,
- rebuild stats,
- raster time,
- layout/paint hotspots,
- memory,
- CPU timeline.
Очень часто разработчик уверен, что у него “проблема в state management”, а на деле:
- тормозит blur,
- убивает FPS большой image decode,
- съедает всё кастомный painter,
- или проседает layout из-за слишком сложного дерева.
Без профилирования оптимизация превращается в шаманство.
Главное, что стоит запомнить
Во Flutter не нужно бояться setState(). Нужно бояться непонятной стоимости обновления.
setState() сам по себе — не проблема. Проблема начинается, когда за одним маленьким изменением тянется слишком большой хвост:
- тяжёлая логика,
- лишний layout,
- дорогой paint,
- широкая зона rebuild,
- неудачные списки,
- или визуальные эффекты, которые дороже, чем кажутся.
И вот это, как мне кажется, одна из самых полезных мыслей для Flutter-разработчика: оптимизация во Flutter — это не борьба с rebuild, а управление объёмом работы после него.
Как показывает практика — rebuild, repaint и почему setState() обычно не главная проблема...
У Flutter есть странная репутация. Одни считают его магией, где всё “летает из коробки”. Другие — фреймворком, который начинает лагать, как только экран становится сложнее счетчика и списка карточек. Правда, как обычно, скучнее и полезнее: Flutter чаще всего тормозит не потому, что вы “слишком много вызываете setState()”, а потому, что неправильно понимаете, что именно в этот момент происходит.
И если этот момент понять, внезапно становится легче писать и быстрые интерфейсы, и более предсказуемый код.
Главная ошибка: путать rebuild и repaint
Когда разработчик говорит: “Я вызвал setState(), и у меня перерисовался весь экран”, обычно он смешивает сразу несколько разных стадий.
Во Flutter есть как минимум четыре важных шага:
- build — framework пересобирает дерево виджетов;
- layout — вычисляет размеры и позиции;
- paint — рисует содержимое;
- compositing / rasterization — готовит и отправляет всё на GPU.
И вот ключевая мысль: rebuild не равен repaint, а repaint не всегда означает тяжёлую работу на GPU.
Widget во Flutter — лёгкая декларация. Само по себе пересоздание дерева виджетов обычно не является катастрофой. Настоящие проблемы начинаются, когда за rebuild тянется дорогой layout, тяжёлый paint, лишние эффекты, большие списки, изображения, шейдеры, blur, тени или просто слишком широкая область обновления.
То есть вопрос не “сколько раз вызвали setState()”, а какой объём работы потянулся за этим вызовом.
Почему setState() демонизируют зря
Вот типичный пример:
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Column(
children: [
const HeavyHeader(),
Text('$count'),
ElevatedButton(
onPressed: () {
setState(() {
count++;
});
},
child: const Text('Increment'),
),
],
),
);
}
}Многие смотрят на это и думают: “Плохо, потому что пересобирается весь Column”.
Но сам rebuild этого поддерева почти наверняка дешёвый. Проблема была бы не в setState(), а в том, что внутри HeavyHeader может оказаться что-то реально тяжёлое: анимации, сложный кастомный paint, картинки, shader mask, blur или лишние layout-проходы.
То есть лечить тут надо не setState(), а границы обновления и реальную стоимость UI.
Где Flutter начинает реально страдать
Вот типичные источники jank, которые встречаются намного чаще, чем “слишком много rebuild”:
1. Тяжёлая работа внутри build()
build() должен быть дешёвым и предсказуемым. Если внутри вы:
- фильтруете большой список,
- сортируете данные,
- парсите JSON,
- собираете сложную карту UI-моделей,
- дергаете синхронную бизнес-логику,
то проблема не в Flutter, а в том, что вы превратили декларацию интерфейса в вычислительный пайплайн.
Плохой пример:
@override
Widget build(BuildContext context) {
final visibleItems = items
.where((e) => e.isActive)
.toList()
..sort((a, b) => a.name.compareTo(b.name));
return ListView.builder(
itemCount: visibleItems.length,
itemBuilder: (_, index) => ItemTile(item: visibleItems[index]),
);
}Если items большой и экран пересобирается часто, вы будете платить за это снова и снова.
Лучше вынести вычисления:
- в состояние,
- в selector,
- в memoization,
- в отдельный слой данных.
2. Слишком широкая область состояния
Когда локальное изменение живёт слишком высоко в дереве, вы заставляете framework заново проходить большой кусок UI без необходимости.
Плохой паттерн — держать всё состояние страницы в одном StatefulWidget, где любое изменение бьёт по всему экрану.
Лучше дробить экран на более мелкие участки и поднимать состояние ровно настолько, насколько нужно, а не “на всякий случай”.
3. Непонимание, когда нужен RepaintBoundary
RepaintBoundary не ускоряет всё подряд. Он полезен, когда у вас есть часть UI, которую не нужно заново перерисовывать, даже если рядом что-то активно меняется.
Например:
- тяжёлый статичный header,
- сложный кастомный график,
- карточка с дорогой отрисовкой,
- вложенная анимация, которую лучше изолировать.
Но если бездумно обложить весь экран RepaintBoundary, можно получить обратный эффект: больше слоёв, больше памяти, меньше пользы.
4. Неправильная работа со списками
Очень много проблем во Flutter приходят не из state management, а из списков.
Типичные ошибки:
- строить весь список сразу вместо ListView.builder;
- использовать shrinkWrap: true там, где он заставляет список измерять себя полностью;
- класть тяжёлые виджеты внутрь элементов списка;
- терять identity элементов из-за неудачной работы с Key.
И отдельно: Key — это не “ускоритель Flutter”. Key нужен прежде всего для сохранения идентичности элементов, а не для магической оптимизации.
Что делать на практике
Вот несколько правил, которые действительно помогают.
1. Делайте дешёвый build()
build() должен описывать UI, а не выполнять тяжёлую работу.
Хороший ментальный вопрос: если build() вызовется 60 раз в секунду, этот код всё ещё безопасен?
Если ответ “нет”, значит туда попало лишнее.
2. Локализуйте изменения
Если меняется только счётчик — не нужно пересобирать весь экран. Если меняется только кнопка — не нужно трогать header. Если обновляется один item в списке — не нужно гонять всю страницу.
Например, вместо одного большого StatefulWidget часто лучше выделить маленький виджет с собственной зоной ответственности:
class LikeButton extends StatefulWidget {
const LikeButton({super.key});
@override
State<LikeButton> createState() => _LikeButtonState();
}
class _LikeButtonState extends State<LikeButton> {
bool liked = false;
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(liked ? Icons.favorite : Icons.favorite_border),
onPressed: () {
setState(() {
liked = !liked;
});
},
);
}
}Такой код не “современнее” сам по себе, но он лучше держит границы обновления.
3. Используйте const везде, где это честно возможно
const — не волшебная таблетка, но очень полезный сигнал для framework: этот объект не меняется, его можно не пересоздавать заново.
const Text('Profile')
const SizedBox(height: 16)
const Icon(Icons.settings)Это не решит проблемы плохой архитектуры, но уменьшит лишний шум в дереве.
4. Разделяйте состояние и отрисовку
Во Flutter легко скатиться в код, где UI, бизнес-логика, фильтрация данных и анимации смешаны в одном build().
Чем чище разделены:
- состояние,
- производные данные,
- эффекты,
- UI,
тем легче контролировать производительность.
И не так важно, используете вы Provider, Riverpod, Bloc, Cubit или что-то ещё. Большинство проблем производительности приходят не из выбранной библиотеки, а из того, насколько точно вы обновляете нужный участок дерева.
5. Профилируйте, а не угадывайте
Это, наверное, самый полезный совет.
Flutter DevTools показывает намного больше, чем кажется:
- frame rendering,
- rebuild stats,
- raster time,
- layout/paint hotspots,
- memory,
- CPU timeline.
Очень часто разработчик уверен, что у него “проблема в state management”, а на деле:
- тормозит blur,
- убивает FPS большой image decode,
- съедает всё кастомный painter,
- или проседает layout из-за слишком сложного дерева.
Без профилирования оптимизация превращается в шаманство.
Главное, что стоит запомнить
Во Flutter не нужно бояться setState(). Нужно бояться непонятной стоимости обновления.
setState() сам по себе — не проблема. Проблема начинается, когда за одним маленьким изменением тянется слишком большой хвост:
- тяжёлая логика,
- лишний layout,
- дорогой paint,
- широкая зона rebuild,
- неудачные списки,
- или визуальные эффекты, которые дороже, чем кажутся.
И вот это, как мне кажется, одна из самых полезных мыслей для Flutter-разработчика: оптимизация во Flutter — это не борьба с rebuild, а управление объёмом работы после него.