
Производительность Flutter-приложения напрямую зависит от качества написанного кода: лишние перестроения UI, тяжёлые операции в основном потоке, неправильная работа со списками и изображениями — всё это ведёт к фризам, падению FPS и ухудшению пользовательского опыта.
В данной статье мы собрали наиболее распространённые ошибки, которые снижают производительность Flutter-приложений, и показали, как их избежать на практике.
Введение
Flutter позволяет за короткое время собрать красивый и функциональный интерфейс. Однако именно эта скорость часто приводит к тому, что разработчики недооценивают важность оптимизации и архитектурных решений на ранних этапах. Пока приложение небольшое, ошибки в производительности могут быть незаметны, но с ростом функциональности они начинают накапливаться и в какой-то момент превращаются в реальные проблемы: интерфейс начинает "дёргаться", анимации становятся менее плавными, увеличивается время отклика, появляются фризы.
Важно понимать, что Flutter работает по принципу частых перерисовок UI, и сам по себе этот механизм эффективен только при условии, что разработчик использует его правильно. Любой лишний build, тяжёлая операция в UI-потоке или неправильная работа со списками напрямую влияет на пользовательский опыт. В отличие от некоторых других платформ, здесь нет "магической оптимизации" — производительность во многом зависит от качества написанного кода.
Игнорирование базовых правил оптимизации может привести к тому, что даже современное устройство будет испытывать трудности при работе с приложением. При этом исправление таких проблем на поздних этапах разработки часто требует значительных усилий: приходится перерабатывать архитектуру, разделять виджеты, переносить бизнес-логику и оптимизировать взаимодействие с данными.
Поэтому гораздо эффективнее изначально писать код с учётом производительности. Это не означает преждевременную оптимизацию, но предполагает соблюдение проверенных практик: минимизация лишних перестроений UI, правильная работа с асинхронностью, использование ленивых списков и разделение ответственности между слоями приложения. Такой подход позволяет не только избежать проблем в будущем, но и делает код более читаемым, поддерживаемым и масштабируемым.
Ниже собраны конкретные рекомендации, как делать не следует, и как будет более оптимально использовать ресурсы во flutter-приложении.
1. Лишние rebuild’ы
Плохо:
class MyScreen extends StatefulWidget { @override State<MyScreen> createState() => _MyScreenState(); } class _MyScreenState extends State<MyScreen> { int counter = 0; @override Widget build(BuildContext context) { return Column( children: [ Text('Counter: $counter'), ElevatedButton( onPressed: () { setState(() { counter++; // пересобирает ВСЁ дерево }); }, child: Text('Increment'), ), ], ); } }
Главная проблема setState — он пересобирает весь subtree текущего StatefulWidget. Если внутри есть тяжёлые виджеты (списки, картинки, сложные layout’ы), это приводит к лишним вычислениям и падению FPS. Даже если изменился один Text, Flutter заново вызовет build() для всех дочерних элементов. Это особенно критично на слабых устройствах. Решение — локализовать обновления. ValueListenableBuilder, StreamBuilder, Selector или BlocBuilder позволяют обновлять только конкретную часть UI. Чем меньше область перерисовки — тем выше производительность. Это ключевая практика для scalable UI.
Хорошо:
class MyScreen extends StatelessWidget { final ValueNotifier<int> counter = ValueNotifier(0); @override Widget build(BuildContext context) { return Column( children: [ ValueListenableBuilder<int>( valueListenable: counter, builder: (_, value, __) { return Text('Counter: $value'); }, ), ElevatedButton( onPressed: () => counter.value++, child: const Text('Increment'), ), ], ); } }
2. Нет const
Плохо:
Widget build(BuildContext context) { return Column( children: [ Text('Hello'), Icon(Icons.home), Padding( padding: EdgeInsets.all(8), child: Text('World'), ), ], ); }
Ключевое слово, модификатор const сообщает Flutter, что виджет неизменяемый и может быть создан на этапе компиляции. Без const каждый rebuild создаёт новый объект, даже если он полностью идентичен предыдущему. Это увеличивает нагрузку на GC (сборщик мусора) и CPU. При большом количестве мелких виджетов (иконки, тексты, padding) это начинает заметно влиять на производительность. Использование const позволяет Flutter переиспользовать уже созданные объекты, уменьшая количество аллокаций. В больших списках или сложных UI это даёт ощутимый прирост производительности и стабильности кадров.
Хорошо:
Widget build(BuildContext context) { return const Column( children: [ Text('Hello'), Icon(Icons.home), Padding( padding: EdgeInsets.all(8), child: Text('World'), ), ], ); }
3. Логика в build()
Плохо:
Widget build(BuildContext context) { final sortedList = items..sort((a, b) => a.compareTo(b)); final filtered = sortedList.where((e) => e > 10).toList(); return ListView( children: filtered.map((e) => Text('$e')).toList(), ); }
Метод build() может вызываться десятки раз в секунду (например, при анимациях или скролле). Если внутри него выполняются тяжёлые операции — сортировка, фильтрация, парсинг — это напрямую влияет на производительность UI. Даже операция sort() на среднем списке может занимать миллисекунды, что критично для 60 FPS (16 ms на кадр). Логика должна выполняться один раз (например, в initState) или в бизнес-слое. build() должен быть максимально лёгким и декларативным — только описание UI. Это фундаментальный принцип Flutter: UI должен разделён с бизнес-логикой.
Хорошо:
late List<int> filtered; @override void initState() { super.initState(); final sorted = [...items]..sort((a, b) => a.compareTo(b)); filtered = sorted.where((e) => e > 10).toList(); } @override Widget build(BuildContext context) { return ListView.builder( itemCount: filtered.length, itemBuilder: (_, i) => Text('${filtered[i]}'), ); }
4. ListView без builder
Плохо:
Widget build(BuildContext context) { return ListView( children: items.map((item) { return ListTile( title: Text(item.title), subtitle: Text(item.subtitle), ); }).toList(), ); }
При использовании children: [...] Flutter создаёт ВСЕ элементы списка сразу, даже если пользователь видит только первые 5–10. Это приводит к лишним аллокациям, загрузке памяти и долгому времени первого рендера. Особенно критично при списках 100+ элементов. ListView.builder создаёт элементы лениво — только те, которые видны на экране. Это drastically снижает нагрузку на CPU и память. Также builder лучше работает с переработкой элементов при скролле. Это стандарт де-факто для любых динамических списков во Flutter.
Хорошо:
Widget build(BuildContext context) { return ListView.builder( itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; return ListTile( title: Text(item.title), subtitle: Text(item.subtitle), ); }, ); }
5. Нет keys
Плохо:
ListView.builder( itemCount: items.length, itemBuilder: (_, index) { final item = items[index]; return ListTile( title: Text(item.title), ); }, );
Без key Flutter не может корректно сопоставить старые и новые элементы при обновлении списка. В результате он может пересоздавать виджеты вместо их обновления, что приводит к лишним rebuild’ам и визуальным багам (например, “прыгающие” элементы). Key позволяет Flutter понимать, какой элемент соответствует какому состоянию. Это особенно важно при:
reorder списка
удалении элементов
анимациях
Использование ValueKey или ObjectKey значительно снижает количество ненужных операций и делает UI стабильным.
Хорошо:
ListView.builder( itemCount: items.length, itemBuilder: (_, index) { final item = items[index]; return ListTile( key: ValueKey(item.id), title: Text(item.title), ); }, );
6. Частый setState (таймер)
Плохо:
Timer.periodic(Duration(milliseconds: 100), (_) { setState(() { progress += 0.01; }); }); Widget build(BuildContext context) { return LinearProgressIndicator(value: progress); }
Частые вызовы setState (например, каждые 16–100 мс) перегружают UI поток. Flutter вынужден постоянно пересчитывать layout и перерисовывать виджеты. Это приводит к пропуску кадров (jank). Для анимаций есть специализированные инструменты: AnimationController, AnimatedBuilder, TweenAnimationBuilder. Они оптимизированы и обновляют только нужные части UI. Также они работают синхронно с кадровой частотой устройства. Использование правильных инструментов для анимаций — ключ к плавному интерфейсу.
Хорошо:
late AnimationController controller; @override void initState() { super.initState(); controller = AnimationController( vsync: this, duration: Duration(seconds: 2), )..repeat(); } Widget build(BuildContext context) { return AnimatedBuilder( animation: controller, builder: (_, __) { return LinearProgressIndicator(value: controller.value); }, ); }
7. Огромный StatefulWidget
Плохо:
Widget build(BuildContext context) { return Column( children: [ Text('Header'), ListView(...), Text('Footer'), ElevatedButton(...), Image.network(url), ], ); }
Когда весь экран — один StatefulWidget, любое изменение состояния приводит к пересборке всей структуры. Это дорого и плохо масштабируется. Разделение UI на маленькие независимые виджеты позволяет:
локализовать rebuild’ы
переиспользовать код
улучшить читаемость
Flutter оптимизирован под композицию — множество маленьких виджетов лучше, чем один большой. Это снижает нагрузку на build и layout фазу. Также это облегчает тестирование и поддержку.
Хорошо:
Widget build(BuildContext context) { return Column( children: const [ Header(), Expanded(child: ItemsList()), Footer(), ], ); } class Header extends StatelessWidget { const Header(); @override Widget build(BuildContext context) { return Text('Header'); } }
8. Future в build()
Плохо:
Widget build(BuildContext context) { return FutureBuilder( future: fetchData(), // каждый раз новый запрос builder: (_, snapshot) { if (!snapshot.hasData) return CircularProgressIndicator(); return Text(snapshot.data.toString()); }, ); }
Если вы вызываете fetchData() прямо в build(), каждый rebuild создаёт новый Future и новый запрос. Это может привести к:
множественным HTTP-запросам
миганию UI
лишней нагрузке на сеть
Правильный подход — создать Future один раз (в initState) и переиспользовать его. FutureBuilder должен работать с уже существующим Future, а не создавать новый. Это гарантирует, что данные загружаются один раз, а UI просто реагирует на результат.
Хорошо:
late Future dataFuture; @override void initState() { super.initState(); dataFuture = fetchData(); } Widget build(BuildContext context) { return FutureBuilder( future: dataFuture, builder: (_, snapshot) { if (!snapshot.hasData) return CircularProgressIndicator(); return Text(snapshot.data.toString()); }, ); }
9. Картинки без кэша
Плохо:
Column( children: [ Image.network(url1), Image.network(url2), Image.network(url3), ], );
Image.network по умолчанию не обеспечивает полноценное кэширование. При каждом rebuild изображение может заново загружаться или декодироваться, что нагружает сеть и CPU. Особенно это заметно в списках. Использование библиотек вроде cached_network_image добавляет:
дисковый кэш
memory cache
placeholder
Это уменьшает количество запросов и ускоряет отображение. Картинки — одна из самых тяжёлых частей UI, поэтому их оптимизация даёт большой прирост производительности.
Хорошо:
Column( children: [ CachedNetworkImage(imageUrl: url1), CachedNetworkImage(imageUrl: url2), CachedNetworkImage(imageUrl: url3), ], );
10. Opacity
Плохо:
Opacity( opacity: 0.5, child: Container( color: Colors.red, width: 100, height: 100, ), );
Opacity создаёт отдельный compositing layer и требует дополнительного прохода рендеринга. Это дорогая операция, особенно если используется часто или внутри списков. В простых случаях лучше использовать прозрачные цвета (withOpacity), которые не требуют отдельного слоя. Если нужна анимация — используйте FadeTransition, он более оптимизирован. Избыточное использование Opacity может сильно нагрузить GPU и привести к падению FPS.
Хорошо:
Container( width: 100, height: 100, color: Colors.red.withOpacity(0.5), );
11. Глубокая вложенность
Плохо:
Container( child: Padding( padding: EdgeInsets.all(8), child: Align( alignment: Alignment.center, child: Column( children: [ Text('Hello'), ], ), ), ), );
Каждый виджет добавляет стоимость на build и layout этапах. Глубокие деревья (10+ уровней) увеличивают время рендеринга и усложняют перерасчёт layout. Часто разработчики используют лишние обёртки (Container, Align, SizedBox), хотя их можно заменить более простыми конструкциями. Упрощение дерева:
ускоряет UI
снижает количество вычислений
делает код читаемее
Flutter быстрый, но не магический — глубина дерева всё равно имеет значение.
Хорошо:
Padding( padding: const EdgeInsets.all(8), child: Center( child: Text('Hello'), ), );
12. Тяжёлый JSON в UI
Плохо:
Widget build(BuildContext context) { final data = jsonDecode(bigJsonString); final items = data['items']; return ListView( children: items.map<Widget>((e) => Text(e.toString())).toList(), ); }
Парсинг большого JSON — CPU-bound операция. Если выполнять её в UI потоке, приложение “зависает” на время обработки. Пользователь видит фриз. В Flutter есть isolates — отдельные потоки для тяжёлых задач. compute() позволяет вынести парсинг в background. Это освобождает UI поток и сохраняет плавность интерфейса. Особенно важно при:
больших API ответах
локальных файлах
сложных вычислениях
Разделение UI и вычислений — обязательная практика для production-приложений.
Хорошо:
Future<List<dynamic>> parseData() async { return compute(parseJson, bigJsonString); } List<dynamic> parseJson(String json) { final data = jsonDecode(json); return data['items']; }
Вывод
Большинство проблем с производительностью возникают не из-за сложности задачи, а из-за мелких решений, которые легко упустить в процессе разработки. Лишний rebuild, тяжёлая операция в основном потоке, список без builder — каждое из этих решений кажется безобидным, пока приложение не начинает тормозить.
Хорошая новость в том, что большинство из описанных проблем решаются относительно просто, если знать, на что обращать внимание. Главное — не откладывать это на потом: чем раньше закладываются правильные практики, тем меньше времени уходит на их исправление в будущем.
