Почему ваш 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. Каждый такой вызов заставляет движок рисовать в отдельный буфер и потом композить. На карточке это незаметно, на списке из карточек — уже нет.
Что делать конкретно:
- вместо
Opacityдля статичной полупрозрачности берите цвет с альфой прямо вContainerилиText.style; - вместо
ClipRRectповерх картинки — скругление черезDecorationImageвBoxDecoration, там оно бесплатное; BoxShadowс большимblurRadiusдороже, чем с маленьким; если тень нужна только для отделения карточки от фона, часто хватает границы в один пиксель.
Про const
Совет «расставьте const» повторяют так часто, что он потерял смысл. Уточню, что он реально делает: константный виджет создаётся один раз и при перестройке родителя сравнивается по идентичности, поэтому его поддерево не пересобирается вообще. Это помогает ровно там, где родитель перестраивается часто, а ребёнок неизменен — иконки, разделители, подписи.
Включите линт и не думайте об этом руками:
linter:
rules:
- prefer_const_constructors
- prefer_const_literals_to_create_immutables
Что не помогает
addAutomaticKeepAlives в надежде «закешировать» элементы делает ровно обратное: удерживает в памяти состояние уехавших за экран элементов и мешает переиспользованию. Включайте его только осознанно, когда внутри элемента есть что-то с дорогой инициализацией вроде видеоплеера.
И cacheExtent побольше — тоже сомнительная идея. Вы просто просите движок собрать больше элементов заранее, то есть переносите джанк с момента скролла на момент открытия экрана.
Если после всего этого список всё ещё тормозит — вот тогда посмотрите на количество элементов. Но по моему опыту до этого пункта доходит примерно один случай из пяти.