5 советов по производительности Flutter, которые сделают ваше приложение быстрым
FlutterPulseЭта статья переведена специально для канала FlutterPulse. В этом канале вы найдёте много интересных вещей, связанных с Flutter. Не забывайте подписываться! 🚀
Давайте будем честными: каждое приложение на Flutter работает плавно, когда вы впервые запускаете flutter run. Новый проект, пустые виджеты, нет вызовов API — всё как по маслу. Затем наступает реальность. Вы добавляете несколько вызовов API, добавляете ListView с 500 элементами, может быть, одну-две анимации… и вдруг ваше "масло" начинает напоминать замороженное масло прямо из холодильника.
Анимации начинают тормозить, как будто работают на Windows 98, прокрутка становится липкой, как руки ребёнка после конфет, и тот самый инженер по тестированию начинает отправлять отчёты об ошибках с скриншотами, похожими на фильмы ужасов. Тем временем пользователи оставляют оценки в 1 звезду, говоря: "приложение медленное, исправьте".
Хорошая новость? Ваше приложение не должно работать как Internet Explorer на модеме. С правильными хитростями вы можете сделать его быстрым — плавным и снова похожим на масло.
1. Перестаньте возвращать виджеты из методов
Во имя "чистого кода" мы часто прячем элементы интерфейса внутри вспомогательных методов — buildButton(), getCard(), makeHeader(). В редакторе это выглядит аккуратно, но под капотом Flutter не переиспользует эти виджеты. Каждый вызов создаёт абсолютно новый виджет, и фреймворку приходится перестраивать и перераспределять больше, чем необходимо. Другими словами, мы сделали код чище для себя, но тяжелее для Flutter.
Проблема — Возврат виджетов из методов
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
buildButton("Login"),
buildButton("Register"),
],
);
}
Widget buildButton(String text) {
return ElevatedButton(
onPressed: () {},
child: Text(text),
);
}
}
Здесь каждый раз при вызове build() Flutter создаёт новые кнопки с нуля — даже когда ничего не изменилось. Плавность постепенно умирает.
Решение: Извлечь в виджеты
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: const [
ActionButton(text: "Login"),
ActionButton(text: "Register"),
],
);
}
}
class ActionButton extends StatelessWidget {
final String text;
const ActionButton({required this.text, super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {},
child: Text(text),
);
}
}
Теперь Flutter может оптимизировать и переиспользовать виджеты, так как они разделены на свои собственные компоненты. Нет ненужных перестроек, нет рывков.
2. Держите build() лёгким
Обычная ошибка — использовать build() как ящик для хлама — просто выбросьте туда всё. Разбор JSON? Выбросьте. Фильтрация списков? Почему бы и нет. Форматирование дат? Конечно, почему бы не замучить Flutter ещё больше. Некоторые даже запускают сетевые вызовы в build(), что похоже на приготовление ужина каждый раз, когда кто-то открывает холодильник. Это работает… пока Flutter не начнёт перестраивать, а затем ваше "плавное приложение" начинает чувствовать себя так, будто тащит пианино в гору.
Проблема: Выполнение работы в build()
class ProductList extends StatelessWidget {
final List<Product> products;
const ProductList({super.key, required this.products});
@override
Widget build(BuildContext context) {
// Фильтрация внутри build()
final discounted = products.where((p) => p.isDiscounted).toList();
return ListView.builder(
itemCount: discounted.length,
itemBuilder: (context, index) {
return Text(discounted[index].name);
},
);
}
}
При каждом перестроении повторяется фильтрация .where() — это расточительно и вызывает торможения, если products большой.
Решение: Предвычисляйте вне build()
class ProductList extends StatefulWidget {
final List<Product> products;
const ProductList({super.key, required this.products});
@override
State<ProductList> createState() => _ProductListState();
}
class _ProductListState extends State<ProductList> {
late List<Product> discounted;
@override
void initState() {
super.initState();
// Предвычисление один раз
discounted = widget.products.where((p) => p.isDiscounted).toList();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: discounted.length,
itemBuilder: (context, index) {
return Text(discounted[index].name);
},
);
}
}
Теперь build() только располагает виджеты — никаких дополнительных вычислений. Гладко, быстро и легче в поддержке.
3. Используйте изолированные процессы для тяжелых задач
У Flutter есть один основной поток интерфейса, и когда вы перегружаете его тяжелыми задачами — например, разбором большого JSON, обработкой чисел или изменением размера изображений — он замирает. Результат? Ваше приложение тормозит сильнее, чем видеозвонок на Wi-Fi отеля.
Решение? Передавайте тяжелые задачи изолированным процессам. Представьте их как способ Flutter выполнять работу в другой комнате, чтобы интерфейс не задыхался.
Проблема: Зависание интерфейса при разборе JSON
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
class JsonFreezeExample extends StatefulWidget {
const JsonFreezeExample({super.key});
@override
State<JsonFreezeExample> createState() => _JsonFreezeExampleState();
}
class _JsonFreezeExampleState extends State<JsonFreezeExample> {
String result = "Нажмите, чтобы загрузить JSON";
Future<void> loadJson() async {
final raw = await rootBundle.loadString('assets/large_file.json');
//Этот разбор выполняется синхронно и требует много ресурсов!
final data = jsonDecode(raw);
setState(() {
result = "Загружено ${data.length} элементов";
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Демонстрация зависания JSON")),
body: Center(child: Text(result)),
floatingActionButton: FloatingActionButton(
onPressed: loadJson,
child: const Icon(Icons.download),
),
);
}
}
Если large_file.json содержит тысячи записей, интерфейс полностью зависнет во время разбора — никаких анимаций, нажатий, ничего, пока разбор не завершится.
Решение: Разбирайте JSON в изолированном процессе (compute)
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
// Функция верхнего уровня, необходимая для compute()
dynamic parseJson(String raw) {
return jsonDecode(raw);
}
class JsonIsolateExample extends StatefulWidget {
const JsonIsolateExample({super.key});
@override
State<JsonIsolateExample> createState() => _JsonIsolateExampleState();
}
class _JsonIsolateExampleState extends State<JsonIsolateExample> {
String result = "Нажмите, чтобы загрузить JSON";
Future<void> loadJson() async {
final raw = await rootBundle.loadString('assets/large_file.json');
// Передаем тяжелый разбор в другой изолированный процесс
final data = await compute(parseJson, raw);
setState(() {
result = "Загружено ${data.length} элементов";
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Демонстрация изолированного процесса JSON")),
body: Center(child: Text(result)),
floatingActionButton: FloatingActionButton(
onPressed: loadJson,
child: const Icon(Icons.download),
),
);
}
}
4. Виджет-луковица
Мы все видели это (или писали 😅): Container > Padding > Container > SizedBox > Padding > Container. Перед тем как вы поймете, ваше дерево виджетов выглядит как луковица — и, как луковицы, заставляет плакать.
Правда в том, что большинство этих лишних оберток на самом деле не делают ничего, кроме замедления перестроения и усложнения чтения кода. Flutter предоставляет множество способов объединения отступов, оформления и компоновки в меньшее количество виджетов — ваше дерево виджетов (и ваши коллеги) будут вам благодарны.
Проблема: Виджет-луковица
class BadNesting extends StatelessWidget {
const BadNesting({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container( // лишний
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16),
child: Container( // еще один
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
"Слишком... много... оберток...",
style: const TextStyle(color: Colors.white),
),
),
),
),
),
),
);
}
}
Это перестраивает несколько слоев виджетов, которые не делают ничего лишнего — Flutter должен проходить и строить все их. Это как завернуть бутерброд в 5 слоев фольги, 3 коробки, а затем удивляться, почему его так долго разворачивать.
Решение: Используйте встроенные свойства как можно чаще.
class GoodNesting extends StatelessWidget {
const GoodNesting({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
padding: const EdgeInsets.all(16), // объедините отступы здесь
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
"Гораздо чище ✨",
style: TextStyle(color: Colors.white),
),
),
),
);
}
}
Здесь вы получаете тот же результат но с половиной дерева виджетов. Чище код, легче сборка, проще отладка.
5. Зомби-слушатели, которых вы забыли убить
Утечки памяти в Flutter обычно проникают, когда вы забываете очищать контроллеры, потоки или слушатели. Сначала все работает нормально, но после нескольких навигаций или прокруток ваше приложение начинает накапливать память, как дракон на кофе. В следующий раз, когда вы заметите, прокрутка тормозит, анимации дергаются, и ваше приложение падает на старых телефонах.
Проблема: Зомби-слушатели
class BadLeak extends StatefulWidget {
const BadLeak({super.key});
@override
State<BadLeak> createState() => _BadLeakState();
}
class _BadLeakState extends State<BadLeak> {
final _controller = PageController();
@override
Widget build(BuildContext context) {
return PageView(
controller: _controller,
children: const [
Text("Страница 1"),
Text("Страница 2"),
],
);
}
}
Здесь _controller никогда не освобождается. Перейдите на другую страницу несколько раз и поздравляю — у вас утечка.
Решение:
class GoodLeak extends StatefulWidget {
const GoodLeak({super.key});
@override
State<GoodLeak> createState() => _GoodLeakState();
}
class _GoodLeakState extends State<GoodLeak> {
final _controller = PageController();
@override
void dispose() {
_controller.dispose(); // Очистка
super.dispose();
}
@override
Widget build(BuildContext context) {
return PageView(
controller: _controller,
children: const [
Text("Страница 1"),
Text("Страница 2"),
],
);
}
}
Очистка — это как мытье посуды — никто этого не любит, но если пропустить, всё быстро становится ужасно.
В конце концов, это мелочи, которые накапливаются — забытые слушатели, вложенные контейнеры, тяжелые методы build(). Как разработчики, мы почти запрограммированы на то, чтобы их игнорировать ("ну, это же всего лишь один setState в build(), что может быть хуже?").
Но как немытая посуда, пропущенные тренировки или "ещё одна серия" в 2 часа ночи, эти мелкие ошибки быстро накапливаются. В отдельности безвредные, но вместе они ухудшат производительность вашего приложения быстрее, чем Internet Explorer на модеме.
Так что чистите свой код, держите его лёгким и помните: Flutter быстрый — это обычно мы, разработчики, замедляем его.
Если вам понравилась эта статья, поделитесь ею с вашими коллегами-разработчиками (особенно с теми, кто всё ещё вкладывает 5 контейнеров для отступов), и подпишитесь на меня для новых хитростей Flutter. До следующего раза — пусть ваши кадры остаются плавными, а сборки — лёгкими.
Есть свой кошмар с производительностью или приём, который я пропустил? Оставьте его в комментариях — мне будет интересно посмеяться, поплакать и, возможно, включить его в будущую статью.