Производительность Flutter-приложения напрямую зависит от качества написанного кода: лишние перестроения UI, тяжёлые операции в основном потоке, неправильная работа со списками и изображениями — всё это ведёт к фризам, падению FPS и ухудшению пользовательского опыта.
В данной статье мы собрали наиболее распространённые ошибки, которые снижают производительность Flutter-приложений, и показали, как их избежать на практике.
В данной статье мы собрали наиболее распространённые ошибки, которые снижают производительность Flutter-приложений, и показали, как их избежать на практике.
Введение
Flutter позволяет за короткое время собрать красивый и функциональный интерфейс. Однако именно эта скорость часто приводит к тому, что разработчики недооценивают важность оптимизации и архитектурных решений на ранних этапах. Пока приложение небольшое, ошибки в производительности могут быть незаметны, но с ростом функциональности они начинают накапливаться и в какой-то момент превращаются в реальные проблемы: интерфейс начинает "дёргаться", анимации становятся менее плавными, увеличивается время отклика, появляются фризы.
Важно понимать, что Flutter работает по принципу частых перерисовок UI, и сам по себе этот механизм эффективен только при условии, что разработчик использует его правильно. Любой лишний build, тяжёлая операция в UI-потоке или неправильная работа со списками напрямую влияет на пользовательский опыт. В отличие от некоторых других платформ, здесь нет "магической оптимизации" — производительность во многом зависит от качества написанного кода.
Игнорирование базовых правил оптимизации может привести к тому, что даже современное устройство будет испытывать трудности при работе с приложением. При этом исправление таких проблем на поздних этапах разработки часто требует значительных усилий: приходится перерабатывать архитектуру, разделять виджеты, переносить бизнес-логику и оптимизировать взаимодействие с данными.
Поэтому гораздо эффективнее изначально писать код с учётом производительности. Это не означает преждевременную оптимизацию, но предполагает соблюдение проверенных практик: минимизация лишних перестроений UI, правильная работа с асинхронностью, использование ленивых списков и разделение ответственности между слоями приложения.
Такой подход позволяет не только избежать проблем в будущем, но и делает код более читаемым, поддерживаемым и масштабируемым.
Ниже собраны конкретные рекомендации, как делать не следует, и как будет более оптимально использовать ресурсы во flutter-приложении.
Важно понимать, что Flutter работает по принципу частых перерисовок UI, и сам по себе этот механизм эффективен только при условии, что разработчик использует его правильно. Любой лишний build, тяжёлая операция в UI-потоке или неправильная работа со списками напрямую влияет на пользовательский опыт. В отличие от некоторых других платформ, здесь нет "магической оптимизации" — производительность во многом зависит от качества написанного кода.
Игнорирование базовых правил оптимизации может привести к тому, что даже современное устройство будет испытывать трудности при работе с приложением. При этом исправление таких проблем на поздних этапах разработки часто требует значительных усилий: приходится перерабатывать архитектуру, разделять виджеты, переносить бизнес-логику и оптимизировать взаимодействие с данными.
Поэтому гораздо эффективнее изначально писать код с учётом производительности. Это не означает преждевременную оптимизацию, но предполагает соблюдение проверенных практик: минимизация лишних перестроений UI, правильная работа с асинхронностью, использование ленивых списков и разделение ответственности между слоями приложения.
Такой подход позволяет не только избежать проблем в будущем, но и делает код более читаемым, поддерживаемым и масштабируемым.
Ниже собраны конкретные рекомендации, как делать не следует, и как будет более оптимально использовать ресурсы во flutter-приложении.
Лишние 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'),
),
],
);
}
}Нет 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'),
),
],
);
}Логика в 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 должен разделён с бизнес-логикой.
при анимациях или скролле). Если внутри него выполняются тяжёлые операции — сортировка, фильтрация, парсинг — это напрямую влияет
на производительность 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]}'),
);
}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.
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),
);
},
);
}Нет keys
Плохо:
ListView.builder(
itemCount: items.length,
itemBuilder: (_, index) {
final item = items[index];
return ListTile(
title: Text(item.title),
);
},
);Без key Flutter не может корректно сопоставить старые и новые элементы при обновлении списка. В результате он может пересоздавать виджеты вместо их обновления, что приводит к лишним rebuild’ам и визуальным багам (например, “прыгающие” элементы). Key позволяет Flutter понимать, какой элемент соответствует какому состоянию. Это особенно важно при:
- reorder списка
- удалении элементов
- анимациях
Хорошо:
ListView.builder(
itemCount: items.length,
itemBuilder: (_, index) {
final item = items[index];
return ListTile(
key: ValueKey(item.id),
title: Text(item.title),
);
},
);Частый 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. Также они работают синхронно с кадровой частотой устройства. Использование правильных инструментов для анимаций — ключ к плавному интерфейсу.
Они оптимизированы и обновляют только нужные части 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);
},
);
}Огромный StatefulWidget
Плохо:
Widget build(BuildContext context) {
return Column(
children: [
Text('Header'),
ListView(...),
Text('Footer'),
ElevatedButton(...),
Image.network(url),
],
);
}Когда весь экран — один StatefulWidget, любое изменение состояния приводит к пересборке всей структуры. Это дорого и плохо масштабируется. Разделение UI на маленькие независимые виджеты позволяет:
- локализовать rebuild’ы
- переиспользовать код
- улучшить читаемость
Хорошо:
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');
}
}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
- лишней нагрузке на сеть
Хорошо:
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());
},
);
}Картинки без кэша
Плохо:
Column(
children: [
Image.network(url1),
Image.network(url2),
Image.network(url3),
],
);Image.network по умолчанию не обеспечивает полноценное кэширование. При каждом rebuild изображение может заново загружаться или декодироваться, что нагружает сеть и CPU. Особенно это заметно в списках. Использование библиотек вроде cached_network_image добавляет:
— одна из самых тяжёлых частей UI, поэтому их оптимизация даёт большой прирост производительности.
- дисковый кэш
- memory cache
- placeholder
— одна из самых тяжёлых частей UI, поэтому их оптимизация даёт большой прирост производительности.
Хорошо:
Column(
children: [
CachedNetworkImage(imageUrl: url1),
CachedNetworkImage(imageUrl: url2),
CachedNetworkImage(imageUrl: url3),
],
);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),
);Глубокая вложенность
Плохо:
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
- снижает количество вычислений
- делает код читаемее
Хорошо:
Padding(
padding: const EdgeInsets.all(8),
child: Center(
child: Text('Hello'),
),
);Тяжёлый 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 ответах
- локальных файлах
- сложных вычислениях
Хорошо:
Future<List<dynamic>> parseData() async {
return compute(parseJson, bigJsonString);
}
List<dynamic> parseJson(String json) {
final data = jsonDecode(json);
return data['items'];
}
Вывод
Большинство проблем с производительностью возникают
не из-за сложности задачи, а из-за мелких решений, которые легко упустить
в процессе разработки. Лишний rebuild, тяжёлая операция в основном потоке, список без builder — каждое из этих решений кажется безобидным, пока приложение не начинает тормозить.
Хорошая новость в том, что большинство из описанных проблем решаются относительно просто, если знать, на что обращать внимание.
Главное — не откладывать это на потом: чем раньше закладываются правильные практики, тем меньше времени уходит на их исправление
в будущем.
не из-за сложности задачи, а из-за мелких решений, которые легко упустить
в процессе разработки. Лишний rebuild, тяжёлая операция в основном потоке, список без builder — каждое из этих решений кажется безобидным, пока приложение не начинает тормозить.
Хорошая новость в том, что большинство из описанных проблем решаются относительно просто, если знать, на что обращать внимание.
Главное — не откладывать это на потом: чем раньше закладываются правильные практики, тем меньше времени уходит на их исправление
в будущем.
