The problem of list optimization (RecyclerView/UITableView) is one of the most common in mobile development. UITableView stutters during fast scrolling — and almost always the cause is not slow hardware, but synchronous JPEG decoding on the main thread in cellForRowAt. Prefetch fires too late, the cell is already requested, and while the image decodes, the frame is dropped. On iPhone SE 2nd gen with its modest memory, this reproduces reliably where on Pro it goes unnoticed. We encounter this situation on every other project — and we have proven methods to solve it.
Why lists stutter even on flagships?
The most common story on Android: RecyclerView with LinearLayoutManager and hundreds of items where in onBindViewHolder you do Picasso.get().load(url).into(imageView) without an explicit placeholder and without cancelling the previous request via tag. During fast scrolling, requests accumulate, old ones are not cancelled, and the UI thread is periodically blocked by callbacks. Switching to Glide with a RequestManager tied to lifecycle and preload() in onScrollStateChanged solves this without any logic changes. In our measurements, this reduces frame load time by 40% on mid-range devices.
On iOS, a similar situation: SDWebImage without SDWebImageAvoidAutoSetImage applies the image on the main thread immediately after loading, regardless of whether the cell is visible. You add sd_setImageWithURL:placeholderImage:options:SDWebImageAvoidAutoSetImage and apply in completion only if indexPath == self.tableView.indexPathForCell(cell) — and the stutter disappears.
The second most common problem is heavy cell height calculations. UITableView.automaticDimension is convenient, but with a complex layout featuring multiple UILabels, it triggers a full systemLayoutSizeFitting on every visible cell. A height cache via [IndexPath: CGFloat] and recalculating only when data changes solves the problem. On Jetpack Compose, LazyColumn without key {} cannot correctly reuse composables when data changes — with submitList of modified items, all visible cells are redrawn instead of just the changed ones.
What prevents achieving 60 fps?
Even on flagships, lists stutter due to lack of prefetch. For example, on iOS without implementing UITableViewDataSourcePrefetching, images start loading only when the cell appears. Setting up prefetchDataSource with data loading for the next 3-5 screens provides a time buffer. Similarly on Android: setting setInitialPrefetchItemCount() for horizontal RecyclerViews inside vertical ones eliminates scroll delays. We recommend increasing cacheExtent in Flutter to 500 pixels — this preloads content beyond the viewport.
How DiffUtil speeds up list updates?
DiffUtil computes the difference between two lists on a background thread and generates a minimal set of updates for RecyclerView. This eliminates unnecessary redraws: for example, when updating one item out of 100, only that item is redrawn, not all visible ones. AsyncListDiffer does this asynchronously without blocking the UI. In practice, using DiffUtil reduces update time by 50% and makes scrolling smooth.
What we do specifically
Android RecyclerView:
- setHasFixedSize(true) if the RecyclerView size does not change when data updates
- setItemViewCacheSize(20) to increase the offscreen cell cache
- RecycledViewPool.setMaxRecycledViews(type, count) when multiple RecyclerViews share the same cell type — pool sharing
- AsyncListDiffer or ListAdapter with DiffUtil.ItemCallback — diff on background thread is mandatory for any dynamic list
- Prefetch via LinearLayoutManager.setInitialPrefetchItemCount() for nested horizontal lists
iOS UITableView / UICollectionView:
- prefetchDataSource — decode and cache data before cellForRowAt
- estimatedRowHeight with a real value (not 44 for cells 120 high) — incorrect estimatedRowHeight causes jumps during scrolling
- prepareForReuse() — mandatory cancellation of all async operations: imageLoadTask?.cancel()
- Offscreen rendering of cells via UIGraphicsImageRenderer for static content (avatars, overlaid icons)
Flutter LazyColumn (ListView.builder):
- itemExtent — if all items have the same height, specifying a fixed itemExtent removes the need to measure each cell
- cacheExtent — increase to 500–1000 pixels for preloading outside the viewport
- AutomaticKeepAliveClientMixin — preserve cell state when scrolling back
How we optimize nested lists? A case study
Horizontal RecyclerView inside a vertical one is a common pattern for Netflix-like interfaces. The typical mistake: each horizontal RecyclerView creates its own RecycledViewPool. When scrolling the vertical list, horizontal RecyclerViews are recycled along with child elements, and on return, their state (scroll position) is lost.
From our practice: a client with a content catalog app had complaints about losing position during scrolling. We moved RecycledViewPool to the activity level and passed it to each horizontal RecyclerView via setRecycledViewPool(). We save LinearLayoutManager.onSaveInstanceState() in ViewModel keyed by position. The result — smooth scrolling and preserved position when scrolling the vertical list.
Image loading library comparison
| Criterion | Android (Glide vs Picasso) | iOS (SDWebImage vs Kingfisher) |
|---|---|---|
| Async loading | Glide: built-in Dispatcher, Picasso: recent only | Both async |
| Disk cache | Glide: multi-threaded, Picasso: single-threaded | SDWebImage: disk/memory, Kingfisher: configurable |
| Lifecycle-aware | Glide: yes (RequestManager), Picasso: no | N/A |
| Prefetch | Glide: preload(), Picasso: no | SDWebImage: prefetchURLs, Kingfisher: no |
| Mass loading performance | Glide 40% faster | SDWebImage 30% faster on cold start |
What the work includes
| Stage | What we do | Result |
|---|---|---|
| Audit | Profile scrolling on real devices (3+ models), identify bottlenecks | Report with metrics (fps, render time) and recommendations |
| Optimization | Implement caching, prefetch, RecyclerView/UITableView settings | Smooth scrolling (60 fps) on all devices |
| Testing | Verify on 5+ devices of varying power | Confirmed stability |
| Documentation | Describe changes, prepare code for CI | Integration without regressions |
Timeframes and guarantee
Audit and optimization of one problematic list: 2–4 days. System-wide work on all lists in the app: 1–2 weeks. We guarantee elimination of scroll stutter: if the effect persists, we rework for free. Our track record: 50+ projects with list optimization on iOS, Android, and Flutter. Contact us for an audit — we will evaluate your project in 2 days.







