Урок по треку Flutter и Dart 7 мин

Производительность: перестроения, кадры, DevTools

Лишние перестроения, const, RepaintBoundary, ленивые списки, DevTools и Impeller.

Плавный интерфейс это 60 кадров в секунду (16 мс на кадр), а на экранах 120 Гц всего около 8 мс. Кадр готовят два потока: UI-поток (Dart: build, раскладка, запись команд отрисовки) и raster-поток (движок превращает команды в пиксели на GPU). Тормозит любой из них, и пользователь видит рывок (jank).

Как найти проблему

  1. Мерить только в profile-режиме (flutter run --profile) на реальном устройстве. Debug-режим работает на JIT с проверками и в разы медленнее, его цифры ничего не значат.
  2. Performance overlay показывает два графика: UI и raster. Красные столбики показывают, какой поток не успевает.
  3. Flutter DevTools: вкладка Performance (временная шкала кадров), CPU Profiler, счётчик перестроений виджетов (Track widget rebuilds), Memory для утечек.

Если не успевает UI-поток, ищем тяжёлый build, лишние перестроения и вычисления в главном изоляте. Если raster, ищем дорогие эффекты: saveLayer (прозрачность Opacity над большим поддеревом, ShaderMask, некоторые клипы), огромные изображения, тени.

Лишние перестроения

  • const-конструкторы. Константный виджет не перестраивается вместе с родителем.
  • Опускайте состояние вниз. setState в корне экрана перестраивает всё. Вынесите меняющуюся часть (таймер, счётчик) в маленький StatefulWidget.
  • Подписывайтесь точечно. context.select в Provider, ref.watch(provider.select(...)) в Riverpod, buildWhen в BLoC, MediaQuery.sizeOf вместо MediaQuery.of.
  • Не создавайте объекты в build без нужды: TextEditingController, Future для FutureBuilder. FutureBuilder(future: api.load()) в build отправит запрос на каждом перестроении. Future создают в initState.
  • Выносите куски в виджеты, а не в методы. Метод _buildHeader() перестраивается вместе с родителем, отдельный const Header() нет, и у него свой BuildContext.

Списки

ListView(children: [...]) строит все элементы сразу. Для длинных списков нужен ListView.builder или ListView.separated: элементы строятся по мере прокрутки. Если высота элементов одинакова, itemExtent или prototypeItem избавляют от измерения каждого. Тяжёлые изображения грузите с размером под экран (cacheWidth, cacheHeight у Image), иначе фото 4000 пикселей декодируется в полном разрешении ради миниатюры.

Отрисовка

  • RepaintBoundary выделяет поддерево в отдельный слой. Часто меняющаяся часть (анимация, индикатор) перерисовывается, не трогая статичный фон. Не ставьте его везде: каждый слой стоит памяти.
  • Прозрачность: Opacity над большим поддеревом дорог. Для анимации берите FadeTransition или AnimatedOpacity, для картинки цвет с альфа-каналом.
  • Impeller: новый движок отрисовки. Он компилирует шейдеры заранее и убирает рывки при первом показе эффекта (shader compilation jank). На iOS он единственный вариант, на Android включён по умолчанию на современных устройствах.

Тяжёлые вычисления

Всё, что дольше нескольких миллисекунд (парсинг большого JSON, фильтрация тысяч записей, обработка картинок), выносите в изолят через compute или Isolate.run. async не помогает: синхронный код внутри async-функции всё равно занимает UI-поток.

Память

Утечки во Flutter чаще всего из-за неотменённых подписок, неосвобождённых контроллеров и замыканий, которые держат State после dispose. DevTools Memory умеет сравнивать снимки кучи, а пакет leak_tracker помогает находить неосвобождённые объекты прямо в widget-тестах.

Что спросят на собесе

  • Как найти причину рывков: какие инструменты и в каком режиме.
  • UI-поток и raster-поток: что делает каждый.
  • Пять способов сократить перестроения.
  • Зачем RepaintBoundary и почему Opacity дорогой.
  • Почему FutureBuilder с future прямо в build это ошибка.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.