Производительность критически важна для пользовательского опыта. Медленное приложение с лагами frustrates пользователей и приводит к потере аудитории. В этом руководстве мы разберём методы оптимизации Flutter приложений.
Performance Tools
Flutter предоставляет множество инструментов для анализа производительности.
Performance Overlay
MaterialApp(
showPerformanceOverlay: true,
home: MyApp(),
)
Performance Overlay показывает: — GPU — время рендеринга графики — UI — время выполнения Dart кода
Зелёный — хорошо (<= 16ms на 60fps), красный — плохо.
Dart DevTools
DevTools — мощный инструмент для profiling.
# Запуск DevTools
flutter pub global run devtools
# Или при запуске приложения
flutter run --profile
DevTools предоставляет: — Flutter Inspector — исследование виджетов — Performance — анализ кадров — Memory — профилирование памяти — Network — анализ запросов
Оптимизация рендеринга
Const конструкторы
Const конструкторы позволяют Flutter переиспользовать виджеты вместо пересоздания.
// Плохо
class BadExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Title'),
Text('Subtitle'),
Icon(Icons.star),
],
);
}
}
// Хорошо
class GoodExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: const [
Text('Title'),
Text('Subtitle'),
Icon(Icons.star),
],
);
}
}
Ключевое правило: используйте const везде, где это возможно.
RepaintBoundary
RepaintBoundary изолирует expensive операции рендеринга.
class ExpensiveWidget extends StatelessWidget {
const ExpensiveWidget({super.key});
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: CustomPaint(
painter: ExpensivePainter(),
size: Size(200, 200),
),
);
}
}
Когда часть виджета пересобирается, только изменённая часть перерисовывается.
ListView.builder вместо ListView
// Плохо — все элементы создаются сразу
class BadListView extends StatelessWidget {
final items = List.generate(10000, (i) => 'Item $i');
@override
Widget build(BuildContext context) {
return ListView(
children: [
for (var item in items)
ListTile(title: Text(item)),
],
);
}
}
// Хорошо — только видимые элементы создаются
class GoodListView extends StatelessWidget {
final items = List.generate(10000, (i) => 'Item $i');
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: Text(items[index]));
},
);
}
}
addAutomaticKeepAlives
Для больших списков отключите автоматическое сохранение состояния:
ListView.builder(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
itemCount: items.length,
itemBuilder: (context, index) {
return ItemWidget(items[index]);
},
)
Keys для оптимизации
Keys помогают Flutter identify виджеты и оптимизировать обновления.
class KeyExample extends StatefulWidget {
const KeyExample({super.key});
@override
State<KeyExample> createState() => _KeyExampleState();
}
class _KeyExampleState extends State<KeyExample> {
final List<Item> items = [];
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
// Используйте ValueKey для уникальной идентификации
return ListTile(
key: ValueKey(items[index].id),
title: Text(items[index].title),
);
},
);
}
}
itemBuilder для повторяющихся виджетов
// Плохо
ListView.builder(
itemBuilder: (context, index) {
return Container(
child: Row(
children: [
Icon(Icons.star),
Text('Item $index'),
],
),
);
},
)
// Хорошо
const itemIcon = Icon(Icons.star);
ListView.builder(
itemBuilder: (context, index) {
return Container(
child: Row(
children: const [
itemIcon,
// Text не может быть const
],
),
);
},
)
Оптимизация состояния
StatefulWidget правильно
// Плохо — весь UI пересобирается при изменении counter
class BadExample extends StatefulWidget {
@override
State<BadExample> createState() => _BadExampleState();
}
class _BadExampleState extends State<BadExample> {
int counter = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Counter: $counter'),
Expanded(
// ListView будет пересобираться при каждом изменении counter!
child: ListView.builder(
itemCount: 1000,
itemBuilder: (context, index) {
return ListTile(title: Text('Item $index'));
},
),
),
ElevatedButton(
onPressed: () => setState(() => counter++),
child: Text('Increment'),
),
],
);
}
}
// Хорошо — только counter пересобирается
class GoodExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: const [
CounterWidget(),
Expanded(
child: StaticListView(),
),
],
);
}
}
class CounterWidget extends StatefulWidget {
const CounterWidget({super.key});
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int counter = 0;
@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $counter'),
ElevatedButton(
onPressed: () => setState(() => counter++),
child: const Icon(Icons.add),
),
],
);
}
}
class StaticListView extends StatelessWidget {
const StaticListView({super.key});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 1000,
itemBuilder: (context, index) {
return ListTile(title: Text('Item $index'));
},
);
}
}
Избегайте setState в build
// Плохо — setState вызывается внутри build
class BadExample extends StatefulWidget {
@override
State<BadExample> createState() => _BadExampleState();
}
class _BadExampleState extends State<BadExample> {
int counter = 0;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
setState(() {
counter++; // Хорошо
});
// Плохо — setState внутри build
if (counter > 10) {
setState(() {
counter = 0;
});
}
},
child: Text('Click'),
);
}
}
ValueNotifier для локального состояния
Для простого состояния используйте ValueNotifier:
class CounterWidget extends StatelessWidget {
final ValueNotifier<int> counter = ValueNotifier<int>(0);
CounterWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
ValueListenableBuilder(
valueListenable: counter,
builder: (context, value, child) {
return Text('Counter: $value');
},
),
ElevatedButton(
onPressed: () => counter.value++,
child: const Icon(Icons.add),
),
],
);
}
}
Оптимизация изображений
Кэширование изображений
dependencies:
cached_network_image: ^3.3.0
CachedNetworkImage(
imageUrl: 'https://example.com/image.jpg',
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
maxWidthDiskCache: 600,
maxHeightDiskCache: 600,
)
Оптимизация размера
Image.network(
'https://example.com/image.jpg',
width: 100,
height: 100,
fit: BoxFit.cover,
// Укажите cacheWidth/cacheHeight для уменьшения размера в памяти
cacheWidth: 200,
cacheHeight: 200,
)
Lazy loading
Загружайте изображения по мере необходимости:
class ImageLazyLoader extends StatelessWidget {
const ImageLazyLoader({super.key});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return CachedNetworkImage(
imageUrl: 'https://example.com/image$index.jpg',
width: 100,
height: 100,
fit: BoxFit.cover,
memCacheWidth: 200,
memCacheHeight: 200,
);
},
);
}
}
Оптимизация памяти
Dispose ресурсов
class ProperDispose extends StatefulWidget {
const ProperDispose({super.key});
@override
State<ProperDispose> createState() => _ProperDisposeState();
}
class _ProperDisposeState extends State<ProperDispose> {
late AnimationController _controller;
late TextEditingController _textController;
late ScrollController _scrollController;
StreamSubscription? _subscription;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
_textController = TextEditingController();
_scrollController = ScrollController();
_subscription = someStream.listen((data) {
// Обработка данных
});
}
@override
void dispose() {
// ВСЕГДА вызывайте dispose для контроллеров и подписок
_controller.dispose();
_textController.dispose();
_scrollController.dispose();
_subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container();
}
}
Lazy loading для больших списков
class LazyList extends StatefulWidget {
const LazyList({super.key});
@override
State<LazyList> createState() => _LazyListState();
}
class _LazyListState extends State<LazyList> {
final List<Item> _items = [];
final ScrollController _controller = ScrollController();
bool _isLoading = false;
@override
void initState() {
super.initState();
_loadMore();
_controller.addListener(_scrollListener);
}
void _scrollListener() {
if (_controller.position.pixels >=
_controller.position.maxScrollExtent - 200) {
_loadMore();
}
}
Future<void> _loadMore() async {
if (_isLoading) return;
setState(() => _isLoading = true);
final newItems = await api.fetch(offset: _items.length);
setState(() {
_items.addAll(newItems);
_isLoading = false;
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _controller,
itemCount: _items.length + (_isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index == _items.length) {
return const Center(child: CircularProgressIndicator());
}
return ItemWidget(_items[index]);
},
);
}
}
Оптимизация памяти для изображений
// Загрузка изображения с оптимизацией памяти
Future<ui.Image> loadOptimizedImage(String url) async {
final completer = Completer<ui.Image>();
final imageStream = Image.network(
url,
// Ограничиваем размер загружаемого изображения
cacheWidth: 1024,
cacheHeight: 1024,
).image.resolve(const ImageConfiguration());
imageStream.addListener(
ImageStreamListener((ImageInfo info, _) {
completer.complete(info.image);
}),
);
return completer.future;
}
Оптимизация сети
Batch запросы
// Плохо — множественные запросы
Future<void> badExample(List<String> ids) async {
for (final id in ids) {
await api.getItem(id);
}
}
// Хорошо — один batch запрос
Future<void> goodExample(List<String> ids) async {
await api.getItems(ids);
}
Кэширование
class CachedApiService {
final Map<String, CachedData> _cache = {};
final ApiService _api;
CachedApiService(this._api);
Future<T> get<T>(
String key,
Future<T> Function() fetch, {
Duration duration = const Duration(minutes: 5),
}) async {
// Проверка кэша
if (_cache.containsKey(key)) {
final cached = _cache[key]!;
if (!cached.isExpired) {
return cached.data as T;
}
_cache.remove(key);
}
// Загрузка данных
final data = await fetch();
// Сохранение в кэш
_cache[key] = CachedData(
data: data,
expiry: DateTime.now().add(duration),
);
return data;
}
void invalidate(String key) {
_cache.remove(key);
}
void clear() {
_cache.clear();
}
}
class CachedData<T> {
final T data;
final DateTime expiry;
CachedData({required this.data, required this.expiry});
bool get isExpired => DateTime.now().isAfter(expiry);
}
Сжатие данных
final response = await http.get(
Uri.parse('https://api.example.com/data'),
headers: {
'Accept-Encoding': 'gzip, deflate',
},
);
Отмена запросов
class CancelableRequest extends StatefulWidget {
const CancelableRequest({super.key});
@override
State<CancelableRequest> createState() => _CancelableRequestState();
}
class _CancelableRequestState extends State<CancelableRequest> {
late Future<void> _requestFuture;
@override
void initState() {
super.initState();
_requestFuture = _fetchData();
}
Future<void> _fetchData() async {
try {
final data = await api.fetchData();
setState(() {
// Обновление UI
});
} catch (e) {
if (!mounted) return; // Проверяем, если widget dispose
// Обработка ошибки
}
}
@override
void dispose() {
// Отмена запроса при dispose
// api.cancelRequest();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _requestFuture,
builder: (context, snapshot) {
// UI
},
);
}
}
Isolates для тяжёлых задач
Compute функция
// Изолированная функция (top-level)
int heavyComputation(int n) {
int result = 0;
for (int i = 0; i < n; i++) {
result += i * i;
}
return result;
}
// Использование в UI
class ComputeWidget extends StatefulWidget {
const ComputeWidget({super.key});
@override
State<ComputeWidget> createState() => _ComputeWidgetState();
}
class _ComputeWidgetState extends State<ComputeWidget> {
int? result;
Future<void> _runComputation() async {
final value = await compute(heavyComputation, 1000000);
setState(() => result = value);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Result: ${result ?? "Not computed"}'),
ElevatedButton(
onPressed: _runComputation,
child: const Text('Compute'),
),
],
);
}
}
Isolate для JSON parsing
String parseJsonInBackground(String jsonString) {
final decoded = json.decode(jsonString);
return decoded.toString();
}
// Использование
final parsed = await compute(parseJsonInBackground, jsonString);
Performance Best Practices
1. Используйте const конструкторы
const SizedBox(height: 16)
const Text('Hello')
const Icon(Icons.star)
2. Минимизируйте rebuild
class OptimizedWidget extends StatelessWidget {
const OptimizedWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: const [
StaticPart(), // const — не пересобирается
DynamicPart(), // Может меняться
],
);
}
}
3. Кэшируйте данные
// Используйте cached_network_image для изображений
// Кэшируйте API ответы
// Сохраняйте данные локально
4. Ленивая загрузка
// ListView.builder вместо ListView
// FutureBuilder для асинхронных данных
// Lazy loading для больших списков
5. Оптимизируйте build метод
class OptimizedBuild extends StatelessWidget {
const OptimizedBuild({required this.items, super.key});
final List<Item> items;
@override
Widget build(BuildContext context) {
// Выносим тяжёлые вычисления из build
final filteredItems = _filterItems(items);
return ListView.builder(
itemCount: filteredItems.length,
itemBuilder: (context, index) {
return ItemWidget(filteredItems[index]);
},
);
}
List<Item> _filterItems(List<Item> items) {
return items.where((item) => item.isActive).toList();
}
}
Заключение
Оптимизация производительности Flutter приложений — это постоянный процесс. Используйте DevTools для профилирования и исправляйте узкие места. Помните о главных принципах:
- Const где возможно
- ListView.builder для больших списков
- RepaintBoundary для expensive виджетов
- Кэширование для данных и изображений
- Isolates для тяжёлых вычислений
- Правильная очистка ресурсов в dispose