Skip to content

Продуктивність у Flutter

Оптимізація продуктивності є критично важливою для створення плавних та чуйних застосунків. Flutter надає інструменти та практики для досягнення 60 FPS (або 120 FPS на сучасних пристроях).

Розуміння рендерингу

Дерево віджетів

dart
// Flutter має три дерева:
// 1. Widget Tree - описує UI (незмінні)
// 2. Element Tree - керує життєвим циклом
// 3. RenderObject Tree - відповідає за малювання

// Оптимізація: мінімізуйте глибину дерева віджетів
// Погано
Container(
  child: Padding(
    padding: EdgeInsets.all(16),
    child: Center(
      child: Text('Hello'),
    ),
  ),
)

// Краще
Padding(
  padding: EdgeInsets.all(16),
  child: Center(
    child: Text('Hello'),
  ),
)

const конструктори

dart
// Використовуйте const для незмінних віджетів
class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Буде перебудовуватися кожного разу
        Text('Hello'),

        // Не буде перебудовуватися
        const Text('Hello'),

        // Власний const віджет
        const MyConstWidget(),
      ],
    );
  }
}

class MyConstWidget extends StatelessWidget {
  const MyConstWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return const Padding(
      padding: EdgeInsets.all(16),
      child: Text('Const widget'),
    );
  }
}

Оптимізація setState

Локалізація setState

dart
// Погано - перебудовує весь екран
class BadExample extends StatefulWidget {
  @override
  _BadExampleState createState() => _BadExampleState();
}

class _BadExampleState extends State<BadExample> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          const ExpensiveWidget(), // Перебудовується без потреби
          Text('Counter: $_counter'),
          const AnotherExpensiveWidget(), // Перебудовується без потреби
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() => _counter++),
        child: Icon(Icons.add),
      ),
    );
  }
}

// Добре - тільки лічильник перебудовується
class GoodExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          const ExpensiveWidget(),
          CounterWidget(), // Окремий stateful віджет
          const AnotherExpensiveWidget(),
        ],
      ),
    );
  }
}

class CounterWidget extends StatefulWidget {
  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Counter: $_counter'),
        ElevatedButton(
          onPressed: () => setState(() => _counter++),
          child: Text('Increment'),
        ),
      ],
    );
  }
}

ValueNotifier та ValueListenableBuilder

dart
class EfficientCounter extends StatefulWidget {
  @override
  _EfficientCounterState createState() => _EfficientCounterState();
}

class _EfficientCounterState extends State<EfficientCounter> {
  final ValueNotifier<int> _counter = ValueNotifier(0);

  @override
  void dispose() {
    _counter.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const ExpensiveWidget(), // Не перебудовується

        ValueListenableBuilder<int>(
          valueListenable: _counter,
          builder: (context, value, child) {
            return Text('Counter: $value');
          },
        ),

        ElevatedButton(
          onPressed: () => _counter.value++,
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

Оптимізація списків

ListView.builder

dart
// Погано - створює всі елементи одразу
ListView(
  children: items.map((item) => ItemWidget(item: item)).toList(),
)

// Добре - створює тільки видимі елементи
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ItemWidget(item: items[index]);
  },
)

// Ще краще - з кешуванням
ListView.builder(
  itemCount: items.length,
  cacheExtent: 500, // Кешувати 500 пікселів поза екраном
  itemBuilder: (context, index) {
    return ItemWidget(item: items[index]);
  },
)

itemExtent для списків фіксованої висоти

dart
// Оптимізація для елементів однакової висоти
ListView.builder(
  itemCount: 1000,
  itemExtent: 80, // Фіксована висота елемента
  itemBuilder: (context, index) {
    return ListTile(title: Text('Item $index'));
  },
)

// Або використовуйте SliverFixedExtentList
CustomScrollView(
  slivers: [
    SliverFixedExtentList(
      itemExtent: 80,
      delegate: SliverChildBuilderDelegate(
        (context, index) => ListTile(title: Text('Item $index')),
        childCount: 1000,
      ),
    ),
  ],
)

RepaintBoundary

dart
// Ізолює перемальовування елемента
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return RepaintBoundary(
      child: ComplexItemWidget(item: items[index]),
    );
  },
)

Оптимізація зображень

Кешування зображень

dart
// Використовуйте cached_network_image
CachedNetworkImage(
  imageUrl: imageUrl,
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.error),
  memCacheWidth: 200, // Обмеження розміру в пам'яті
)

// Попереднє завантаження
precacheImage(NetworkImage(imageUrl), context);

// Очищення кешу зображень
PaintingBinding.instance.imageCache.clear();
PaintingBinding.instance.imageCache.clearLiveImages();

Правильний розмір зображень

dart
// Вказуйте розміри для оптимізації пам'яті
Image.network(
  imageUrl,
  width: 200,
  height: 200,
  fit: BoxFit.cover,
  cacheWidth: 400, // Декодувати в менший розмір
  cacheHeight: 400,
)

Оптимізація анімацій

AnimatedBuilder замість setState

dart
// Погано
class _BadAnimationState extends State<BadAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 1),
      vsync: this,
    )..addListener(() {
      setState(() {}); // Перебудовує весь віджет
    });
    _animation = Tween(begin: 0.0, end: 1.0).animate(_controller);
  }

  @override
  Widget build(BuildContext context) {
    return Opacity(
      opacity: _animation.value,
      child: ExpensiveWidget(), // Перебудовується кожен кадр
    );
  }
}

// Добре
class _GoodAnimationState extends State<GoodAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 1),
      vsync: this,
    );
  }

  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: _controller,
      child: const ExpensiveWidget(), // Не перебудовується
    );
  }
}

Transform замість Container

dart
// Погано - викликає relayout
AnimatedBuilder(
  animation: _animation,
  builder: (context, child) {
    return Container(
      margin: EdgeInsets.only(left: _animation.value * 100),
      child: child,
    );
  },
  child: MyWidget(),
)

// Добре - використовує compositing
AnimatedBuilder(
  animation: _animation,
  builder: (context, child) {
    return Transform.translate(
      offset: Offset(_animation.value * 100, 0),
      child: child,
    );
  },
  child: MyWidget(),
)

Lazy loading

Відкладене завантаження віджетів

dart
class LazyWidget extends StatefulWidget {
  @override
  _LazyWidgetState createState() => _LazyWidgetState();
}

class _LazyWidgetState extends State<LazyWidget> {
  bool _isLoaded = false;

  @override
  void initState() {
    super.initState();
    // Скачать після побудови кадру
    WidgetsBinding.instance.addPostFrameCallback((_) {
      setState(() => _isLoaded = true);
    });
  }

  @override
  Widget build(BuildContext context) {
    if (!_isLoaded) {
      return const SizedBox.shrink();
    }
    return ExpensiveWidget();
  }
}

Visibility для невидимих віджетів

dart
// Повністю видаляє віджет з дерева
Visibility(
  visible: _isVisible,
  maintainState: false,
  maintainAnimation: false,
  maintainSize: false,
  child: ExpensiveWidget(),
)

// Зберігає стан, але не рендерить
Offstage(
  offstage: !_isVisible,
  child: ExpensiveWidget(),
)

Важкі обчислення

compute для CPU-інтенсивних задач

dart
import 'dart:isolate';
import 'package:flutter/foundation.dart';

// Функція для виконання в окремому isolate
List<int> heavyComputation(int count) {
  final result = <int>[];
  for (var i = 0; i < count; i++) {
    // Важкі обчислення
    result.add(i * i);
  }
  return result;
}

// Використання
class ComputeExample extends StatefulWidget {
  @override
  _ComputeExampleState createState() => _ComputeExampleState();
}

class _ComputeExampleState extends State<ComputeExample> {
  List<int>? _result;
  bool _isLoading = false;

  Future<void> _runComputation() async {
    setState(() => _isLoading = true);

    // Виконується в окремому isolate
    final result = await compute(heavyComputation, 1000000);

    setState(() {
      _result = result;
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        if (_isLoading)
          CircularProgressIndicator()
        else
          ElevatedButton(
            onPressed: _runComputation,
            child: Text('Обчислити'),
          ),
        if (_result != null)
          Text('Результат: ${_result!.length} елементів'),
      ],
    );
  }
}

Інструменти профілювання

Flutter DevTools

dart
// Запуск з профілюванням
// flutter run --profile

// Відкриття DevTools
// flutter pub global activate devtools
// flutter pub global run devtools

// Використання Timeline
import 'dart:developer' as developer;

void expensiveOperation() {
  developer.Timeline.startSync('expensiveOperation');
  // Код операції
  developer.Timeline.finishSync();
}

Performance Overlay

dart
MaterialApp(
  showPerformanceOverlay: true, // Показати оверлей продуктивності
  checkerboardRasterCacheImages: true, // Перевірка кешування
  checkerboardOffscreenLayers: true, // Перевірка офскрін шарів
  home: MyHomePage(),
)

Логування перебудов

dart
// У debug режимі
import 'package:flutter/rendering.dart';

void main() {
  debugPrintRebuildDirtyWidgets = true; // Логувати перебудови
  debugPrintLayouts = true; // Логувати layout
  debugPrintPaintingMessages = true; // Логувати малювання

  runApp(MyApp());
}

Оптимізація пам'яті

Звільнення ресурсів

dart
class ResourceWidget extends StatefulWidget {
  @override
  _ResourceWidgetState createState() => _ResourceWidgetState();
}

class _ResourceWidgetState extends State<ResourceWidget> {
  late AnimationController _controller;
  late ScrollController _scrollController;
  late TextEditingController _textController;
  StreamSubscription? _subscription;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    _textController = TextEditingController();
    _subscription = someStream.listen((_) {});
  }

  @override
  void dispose() {
    // Завжди звільняйте ресурси!
    _controller.dispose();
    _scrollController.dispose();
    _textController.dispose();
    _subscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

AutomaticKeepAliveClientMixin

dart
// Зберігає стан при прокрутці в TabBarView
class KeepAliveWidget extends StatefulWidget {
  @override
  _KeepAliveWidgetState createState() => _KeepAliveWidgetState();
}

class _KeepAliveWidgetState extends State<KeepAliveWidget>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context); // Обов'язково викликати
    return ExpensiveWidget();
  }
}

Чек-лист оптимізації

dart
/*
1. Віджети:
   ✓ Використовуйте const конструктори
   ✓ Розбивайте великі віджети на менші
   ✓ Локалізуйте setState
   ✓ Використовуйте keys правильно

2. Списки:
   ✓ Використовуйте ListView.builder
   ✓ Вказуйте itemExtent для однакових елементів
   ✓ Використовуйте RepaintBoundary

3. Зображення:
   ✓ Кешуйте мережеві зображення
   ✓ Вказуйте cacheWidth/cacheHeight
   ✓ Використовуйте правильний формат

4. Анімації:
   ✓ Використовуйте AnimatedBuilder
   ✓ Перевагу Transform над Container
   ✓ Уникайте setState в animation listeners

5. Обчислення:
   ✓ Використовуйте compute для важких задач
   ✓ Уникайте блокування UI thread

6. Пам'ять:
   ✓ Звільняйте контролери в dispose
   ✓ Скасовуйте підписки
   ✓ Очищайте кеш при потребі
*/

Висновок

Оптимізація продуктивності у Flutter вимагає розуміння того, як Flutter рендерить UI та керує станом. Використовуйте інструменти профілювання для виявлення проблем та застосовуйте найкращі практики для створення швидких та ефективних застосунків.