Оптимизация списков (RecyclerView/UITableView) — устранение дёрганья

Проблема оптимизации списков (RecyclerView/UITableView) — одна из самых частых в мобильной разработке. UITableView дёргается при быстром скролле — и почти всегда причина не в медленном железе, а в синхронной декодировке JPEG на main thread в cellForRowAt. Prefetch срабатывает слишком поздно, ячейка

Разработка и поддержка любых видов мобильных приложений:

Информационные и развлекательные мобильные приложения
Новостные приложения, игры, справочники, онлайн-каталоги, погодные, фитнес и здоровье, туристические, образовательные, социальные сети и мессенджеры, квиз, блоги и подкасты, форумы, агрегаторы
Мобильные приложения электронной коммерции
Интернет-магазины, B2B-приложения, маркетплейсы, онлайн-обменники, кэшбэк-сервисы, биржи, дропшиппинг-платформы, программы лояльности, доставка еды и товаров, платежные системы
Мобильные приложения для управления бизнес-процессами
CRM-системы, ERP-системы, управление проектами, инструменты для команды продаж, учет финансов, управление производством, логистика и доставка, управление персоналом, системы мониторинга данных
Мобильные приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, платформы предоставления электронных услуг, платформы кешбека, видеохостинги, тематические порталы, платформы онлайн-бронирования и записи, платформы онлайн-торговли

Это лишь некоторые из типы мобильных приложений, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента.

Услуги, которые мы предлагаем
Показано 1 из 1Все 1734 услуг
Оптимизация списков (RecyclerView/UITableView) — устранение дёрганья
Средний
~2-3 дня

Наши компетенции:

Часто задаваемые вопросы

Последние работы

  • image_mobile-applications_feedme_467_0.webp
    Разработка мобильного приложения для компании FEEDME
    894
  • image_mobile-applications_xoomer_471_0.webp
    Разработка мобильного приложения для компании XOOMER
    782
  • image_mobile-applications_rhl_428_0.webp
    Разработка мобильного приложения для компании RHL
    1216
  • image_mobile-applications_zippy_411_0.webp
    Разработка мобильного приложения для компании ZIPPY
    1079
  • image_mobile-applications_affhome_429_0.webp
    Разработка мобильного приложения для компании Affhome
    1002
  • image_mobile-applications_flavors_409_0.webp
    Разработка мобильного приложения для компании FLAVORS
    597

Проблема оптимизации списков (RecyclerView/UITableView) — одна из самых частых в мобильной разработке. UITableView дёргается при быстром скролле — и почти всегда причина не в медленном железе, а в синхронной декодировке JPEG на main thread в cellForRowAt. Prefetch срабатывает слишком поздно, ячейка уже запрошена, и пока изображение декодируется — кадр пропущен. На iPhone SE 2nd gen с его скромной памятью это воспроизводится стабильно там, где на Pro не замечается вовсе. Мы сталкиваемся с такой ситуацией на каждом втором проекте — и у нас есть проверенные методы решения.

Почему списки тормозят даже на флагманах?

Самая частая история на Android: RecyclerView с LinearLayoutManager и сотнями элементов, где в onBindViewHolder выполняется Picasso.get().load(url).into(imageView) без явного placeholder и без отмены предыдущего запроса через tag. При быстром скролле запросы накапливаются, старые не отменяются, UI-поток периодически блокируется колбэками. Переход на Glide с RequestManager, привязанным к lifecycle и preload() в onScrollStateChanged, решает это без каких-либо изменений в логике. По нашим замерам, это снижает время загрузки кадра на 40% на устройствах со средним уровнем производительности.

На iOS аналогичная ситуация: SDWebImage без SDWebImageAvoidAutoSetImage применяет изображение на main thread сразу после загрузки, вне зависимости от того, видна ли ячейка. Добавляешь sd_setImageWithURL:placeholderImage:options:SDWebImageAvoidAutoSetImage и применяешь в completion только если indexPath == self.tableView.indexPathForCell(cell) — и дёрганье исчезает.

Вторая по частоте проблема — тяжёлые вычисления высоты ячейки. UITableView.automaticDimension удобен, но при сложном layout с несколькими UILabel запускает полный systemLayoutSizeFitting на каждую видимую ячейку. Кэш высот через [IndexPath: CGFloat] и пересчёт только при изменении данных решает проблему. На Jetpack Compose LazyColumn без key {} не может корректно переиспользовать composable при изменении данных — при submitList с изменёнными элементами перерисовываются все видимые ячейки вместо изменённых.

Что мешает достичь 60 fps?

Даже на флагманах списки тормозят из-за отсутствия prefetch. Например, на iOS без реализации UITableViewDataSourcePrefetching изображения начинают загружаться только при появлении ячейки. Настройка prefetchDataSource с загрузкой данных для следующих 3-5 экранов даёт запас времени. Аналогично на Android: установка setInitialPrefetchItemCount() для горизонтальных RecyclerView внутри вертикального устраняет задержки скролла. Мы рекомендуем увеличить cacheExtent во Flutter до 500 пикселей — это позволяет подгружать контент за пределами viewport.

Как DiffUtil ускоряет обновление списков?

DiffUtil вычисляет разницу между двумя списками на фоновом потоке и генерирует минимальный набор обновлений для RecyclerView. Это исключает лишние перерисовки: например, при обновлении одного элемента из 100 перерисовывается только он, а не все видимые. AsyncListDiffer делает это асинхронно, не блокируя UI. На практике использование DiffUtil уменьшает время обновления на 50% и делает скролл плавным.

Что делаем конкретно

Android RecyclerView:

  • setHasFixedSize(true) если размер RecyclerView не меняется при обновлении данных
  • setItemViewCacheSize(20) для увеличения offscreen кэша ячеек
  • RecycledViewPool.setMaxRecycledViews(type, count) при нескольких RecyclerView с одинаковым типом ячеек — шаринг пула
  • AsyncListDiffer или ListAdapter с DiffUtil.ItemCallback — diff на фоновом потоке обязателен для любого динамического списка
  • Prefetch через LinearLayoutManager.setInitialPrefetchItemCount() для nested horizontal списков

iOS UITableView / UICollectionView:

  • prefetchDataSource — декодируем и кэшируем данные до cellForRowAt
  • estimatedRowHeight с реальным значением (не 44 для ячеек высотой 120) — неправильный estimatedRowHeight вызывает прыжки при скролле
  • prepareForReuse() — обязательная отмена всех async-операций: imageLoadTask?.cancel()
  • Offscreen rendering ячеек через UIGraphicsImageRenderer для статичного контента (аватары, иконки с наложением)

Flutter LazyColumn (ListView.builder):

  • itemExtent — если все элементы одной высоты, указание фиксированного itemExtent убирает необходимость measure каждой ячейки
  • cacheExtent — увеличиваем до 500–1000 пикселей для preloading вне viewport
  • AutomaticKeepAliveClientMixin — сохраняем состояние ячеек при скролле назад

Как мы оптимизируем вложенные списки? Кейс из практики

Горизонтальный RecyclerView внутри вертикального — распространённый паттерн для «Netflix-like» интерфейсов. Типичная ошибка: каждый горизонтальный RecyclerView создаёт свой RecycledViewPool. При скролле вертикального списка горизонтальные ресайклятся вместе с дочерними элементами, и при возврате ячейки их состояние (позиция скролла) теряется.

Из нашей практики: у клиента с приложением-каталогом контента были жалобы на потерю позиции при скролле. Мы вынесли RecycledViewPool на уровень активности и передали в каждый горизонтальный RecyclerView через setRecycledViewPool(). Сохраняем LinearLayoutManager.onSaveInstanceState() в ViewModel по ключу позиции. Итог — плавный скролл и сохранение позиции при прокрутке вертикального списка.

Сравнение библиотек для загрузки изображений

Критерий Android (Glide vs Picasso) iOS (SDWebImage vs Kingfisher)
Асинхронная загрузка Glide: встроенный Dispatcher, Picasso: с недавних пор Оба асинхронны
Кэш на диске Glide: многопоточный, Picasso: однопоточный SDWebImage: диск/память, Kingfisher: настраиваемый
Lifecycle-aware Glide: есть (RequestManager), Picasso: нет N/A
Prefetch Glide: preload(), Picasso: нет SDWebImage: prefetchURLs, Kingfisher: нет
Производительность массовой загрузки Glide быстрее на 40% SDWebImage быстрее на 30% при холодном старте

Что входит в работу

Этап Что делаем Результат
Аудит Профилируем скролл на реальных устройствах (3+ модели), находим узкие места Отчёт с метриками (fps, время рендеринга) и рекомендациями
Оптимизация Внедряем кэши, prefetch, настройки RecyclerView/UITableView Плавный скролл (60 fps) на всех устройствах
Тестирование Проверяем на 5+ устройствах разной мощности Подтверждение стабильности
Документация Описываем изменения, готовим код для CI Интеграция без регрессий

Сроки и гарантия

Аудит и оптимизация одного проблемного списка — 2–4 дня. Системная работа со всеми списками в приложении — 1–2 недели. Мы гарантируем устранение дёрганья при скролле: если эффект остаётся — дорабатываем бесплатно. Наш опыт — 50+ проектов с оптимизацией списков на iOS, Android и Flutter. Свяжитесь с нами для аудита — оценим ваш проект за 2 дня.