Плавный интерфейс это 60 кадров в секунду (16 мс на кадр), а на экранах 120 Гц всего около 8 мс. Кадр готовят два потока: UI-поток (Dart: build, раскладка, запись команд отрисовки) и raster-поток (движок превращает команды в пиксели на GPU). Тормозит любой из них, и пользователь видит рывок (jank).
Как найти проблему
- Мерить только в profile-режиме (
flutter run --profile) на реальном устройстве. Debug-режим работает на JIT с проверками и в разы медленнее, его цифры ничего не значат. - Performance overlay показывает два графика: UI и raster. Красные столбики показывают, какой поток не успевает.
- 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это ошибка.