5 советов по производительности Flutter, которые сделают ваше приложение быстрым

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. До следующего раза — пусть ваши кадры остаются плавными, а сборки — лёгкими.

Есть свой кошмар с производительностью или приём, который я пропустил? Оставьте его в комментариях — мне будет интересно посмеяться, поплакать и, возможно, включить его в будущую статью.

Report Page