Рубрики
Flutter

Оптимизация производительности Flutter приложений 2025

Полное руководство по оптимизации производительности Flutter приложений: profiling, оптимизация рендеринга, памяти и сети.

Производительность критически важна для пользовательского опыта. Медленное приложение с лагами 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