Какие причины тормозов RecyclerView и как их оптимизировать
Что проверяют
Знаете ли вы, откуда берутся пропуски кадров при прокрутке, и умеете ли подтвердить гипотезу измерением. Совет «используйте ViewHolder» не засчитают: без него RecyclerView просто не работает. Следом часто спрашивают про вложенные горизонтальные списки или про то, как вы измеряли результат.
Как отвечать
На кадр при 60 Гц у главного потока около 16 мс, при 120 Гц около 8 мс, всё сверх этого даёт рывок. Основные причины:
- тяжёлый
onBindViewHolder: форматирование дат, парсинг, запросы к базе, декодирование картинок в главном потоке. Готовьте данные к показу заранее, во ViewModel, а bind оставьте простым присваиванием; - сложные макеты: глубокая вложенность,
layout_weightво вложенныхLinearLayout, двойные проходы измерения. ПомогаютConstraintLayoutи тег<merge>; notifyDataSetChanged(): помечает все элементы недействительными, перепривязывает видимые и отключает анимации. Вместо негоListAdapterсDiffUtil.ItemCallback: разница считается в фоне, обновляются только изменённые позиции;- изображения: Glide или Coil с загрузкой под размер
ImageView, а не полноразмерный битмап; - вложенные списки: общий
RecycledViewPoolу горизонтальных списков иsetInitialPrefetchItemCountу внутреннегоLinearLayoutManager; RecyclerViewвнутриNestedScrollView: список создаёт все элементы сразу, и переиспользование пропадает.
Работают и мелочи: setHasFixedSize(true), если размер списка не зависит от содержимого, setHasStableIds(true), setItemViewCacheSize при частой прокрутке туда и обратно.
Замеры до и после: CPU Profiler и System Trace в Android Studio, Perfetto, Macrobenchmark с FrameTimingMetric на release-сборке.
Где спотыкаются
Отвечают из эпохи ListView про findViewById в каждом getView. Для RecyclerView это давно не причина.
Меряют плавность на debug-сборке, где код медленнее и цифры не показательны.
Оптимизируют вслепую, не открыв профайлер. Часто тормозит одна картинка или SimpleDateFormat, который создаётся на каждый bind.