Optimizing Mobile App Lists (RecyclerView/UITableView/ListView)
UITableView jerks on fast scroll — and almost always the cause isn't "slow hardware" but synchronous JPEG decoding on main thread in cellForRowAt. Prefetch fires too late, cell already requested, and while image decodes — frame skips. On iPhone SE 2nd gen with modest memory this reproduces stably where on Pro isn't noticed at all.
Real Stutter Causes
Most common Android story: RecyclerView with LinearLayoutManager and hundreds of elements where onBindViewHolder executes Picasso.get().load(url).into(imageView) without explicit placeholder and without canceling previous request via tag. On fast scroll requests accumulate, old ones aren't canceled, UI thread periodically blocks from callbacks. Switch to Glide with RequestManager bound to lifecycle and preload() in onScrollStateChanged solves it without logic changes.
Analogous iOS situation: SDWebImage without SDWebImageAvoidAutoSetImage applies image on main thread immediately after loading regardless of visibility. Add sd_setImageWithURL:placeholderImage:options:SDWebImageAvoidAutoSetImage and apply in completion only if indexPath == self.tableView.indexPathForCell(cell) — stuttering disappears.
Second most common — heavy cell height calculations. UITableView.automaticDimension convenient, but with complex layout with multiple UILabel runs full systemLayoutSizeFitting on each visible cell. Height cache via [IndexPath: CGFloat] and recalc only on data change solves problem.
On Jetpack Compose LazyColumn without key {} can't correctly reuse composable when data changes — submitList with changed elements rerenders all visible cells instead of changed ones.
What We Do Specifically
Android RecyclerView:
-
setHasFixedSize(true)if RecyclerView size doesn't change on data update -
setItemViewCacheSize(20)to increase offscreen cell cache -
RecycledViewPool.setMaxRecycledViews(type, count)with multiple RecyclerView of same cell type — pool sharing -
AsyncListDifferorListAdapterwithDiffUtil.ItemCallback— diff on background thread mandatory for any dynamic list - Prefetch via
LinearLayoutManager.setInitialPrefetchItemCount()for nested horizontal lists
iOS UITableView / UICollectionView:
-
prefetchDataSource— decode and cache data beforecellForRowAt -
estimatedRowHeightwith real value (not44for 120-height cells) — wrongestimatedRowHeightcauses scroll jumps -
prepareForReuse()— must cancel all async operations:imageLoadTask?.cancel() - Offscreen rendering cells via
UIGraphicsImageRendererfor static content (avatars, overlaid icons)
Flutter LazyColumn (ListView.builder):
-
itemExtent— if all items same height, specifying fixeditemExtentremoves need to measure each -
cacheExtent— increase to500–1000pixels for preloading outside viewport -
AutomaticKeepAliveClientMixin— preserves cell state when scrolling back
Case with Nested Lists
Horizontal RecyclerView inside vertical — common Netflix-like interface pattern. Typical mistake: each horizontal RecyclerView creates own RecycledViewPool. On vertical scroll, horizontal ones recycle with children, and on returning cell's state (scroll position) is lost.
Solution: extract RecycledViewPool at activity level and pass to each horizontal RecyclerView via setRecycledViewPool(). Save LinearLayoutManager.onSaveInstanceState() in ViewModel by position key. Result — smooth scroll and position preservation on vertical list scroll.
Timelines
Audit and optimization of one problem list — 2–4 days. Systematic work on all lists in app — 1–2 weeks.







