flutterprincess

Почему ваш ListView лагает на двухстах карточках

Ко мне регулярно приходят с формулировкой «список тормозит, наверное надо пагинацию». Пагинация в половине случаев не нужна. Двести элементов — это не много, ListView.builder держит их без единого пропущенного кадра, если элементы дешёвые. Проблема почти всегда в том, что они не дешёвые.

Сначала измерьте

Прежде чем что-то менять, соберите профиль. Только в release или profile режиме — в debug всё медленно по определению, и вы будете чинить несуществующую проблему.

flutter run --profile

Дальше открывайте DevTools, вкладку Performance, и скроллите список. Вам нужны два числа на каждый кадр: время UI и время Raster. Если растёт UI — вы слишком много считаете в build. Если растёт Raster — вы слишком много рисуете, и это обычно тени, клипы и прозрачность.

Случай первый: тяжёлый build

Классика — форматирование даты и цены прямо в методе сборки:

Widget build(BuildContext context) {
  final fmt = DateFormat('d MMMM y', 'ru');
  return Text(fmt.format(order.createdAt));
}

Создание DateFormat стоит заметно дороже, чем кажется, и вы делаете это на каждый кадр для каждой видимой карточки. Умножьте на десять карточек на экране и на 120 кадров в секунду. Выносите такие вещи в статическое поле или считайте один раз при загрузке данных.

Второй частый источник — setState на уровне экрана вместо уровня элемента. Когда пользователь ставит лайк на одной карточке, а вы дёргаете setState у родителя, пересобирается весь список. Локализуйте перестройку: отдельный маленький виджет с собственным состоянием или ValueListenableBuilder вокруг конкретной иконки.

Случай второй: тяжёлый растр

Здесь главный виновник — saveLayer. Его вызывают Opacity, ShaderMask, ClipRRect с антиалиасингом и BackdropFilter. Каждый такой вызов заставляет движок рисовать в отдельный буфер и потом композить. На карточке это незаметно, на списке из карточек — уже нет.

Что делать конкретно:

Про const

Совет «расставьте const» повторяют так часто, что он потерял смысл. Уточню, что он реально делает: константный виджет создаётся один раз и при перестройке родителя сравнивается по идентичности, поэтому его поддерево не пересобирается вообще. Это помогает ровно там, где родитель перестраивается часто, а ребёнок неизменен — иконки, разделители, подписи.

Включите линт и не думайте об этом руками:

linter:
  rules:
    - prefer_const_constructors
    - prefer_const_literals_to_create_immutables

Что не помогает

addAutomaticKeepAlives в надежде «закешировать» элементы делает ровно обратное: удерживает в памяти состояние уехавших за экран элементов и мешает переиспользованию. Включайте его только осознанно, когда внутри элемента есть что-то с дорогой инициализацией вроде видеоплеера.

И cacheExtent побольше — тоже сомнительная идея. Вы просто просите движок собрать больше элементов заранее, то есть переносите джанк с момента скролла на момент открытия экрана.

Если после всего этого список всё ещё тормозит — вот тогда посмотрите на количество элементов. Но по моему опыту до этого пункта доходит примерно один случай из пяти.