A list scrolls with stutters. How do you find and fix the problem?
TL;DR
Stutters are hitches: a frame is not ready in time. At 60 Hz you have about 16.7 ms per frame. At 120 Hz (ProMotion), about 8.3 ms.
Common causes: heavy cellForRowAt, image decoding on the main thread, complex Auto Layout, and offscreen rendering (shadows, masks).
Process: measure with Instruments, fix the biggest cause, measure again.
Step 1: measure
- Instruments > Animation Hitches: shows each hitch and whether it came from your app (commit phase) or the render server.
- Time Profiler: shows which functions run on the main thread while scrolling.
- Test on an older, real device in a Release build.
- Simulator numbers are not reliable for this.
Step 2: common causes and fixes
1. Too much work when configuring a cell
// Bad: formatter created and text parsed for every cell
func configure(with post: Post) {
let formatter = DateFormatter()
formatter.dateStyle = .medium
dateLabel.text = formatter.string(from: post.date)
bodyLabel.attributedText = try? NSAttributedString(markdown: post.body)
}Fix: reuse formatters, and prepare display data before scrolling, in the view model.
struct PostRow {
let title: String
let dateText: String // already formatted
let body: NSAttributedString // already parsed
}2. Image decoding on the main thread
Setting a large JPEG on an image view decodes it on the main thread at display time. A big photo can take many milliseconds.
// Decode and resize off the main thread (iOS 15+)
let thumbnail = await image.byPreparingThumbnail(ofSize: CGSize(width: 120, height: 120))
imageView.image = thumbnailAlso cache the result. Do not decode the same image again for each reuse.
3. Offscreen rendering
Some effects force the GPU to draw into an extra buffer. Shadows without a path are a classic example.
// Bad: the system must compute the shadow shape every frame
cardView.layer.shadowOpacity = 0.2
// Good: give the shape
cardView.layer.shadowPath = UIBezierPath(roundedRect: cardView.bounds, cornerRadius: 12).cgPathSet shadowPath in layoutSubviews so it follows size changes.
Masks and shouldRasterize used wrongly can also hurt.
4. Expensive layout
- Deep nested stack views in every cell.
- Many constraints added and removed during configuration.
- Self-sizing cells that measure complex text.
Fix: flatter hierarchies, activate constraints once in init, and only change constants or isHidden later.
Give a good estimatedRowHeight (or estimated item size) so the scroll bar does not jump.
5. Prefetching
extension FeedViewController: UICollectionViewDataSourcePrefetching {
func collectionView(_ cv: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
for path in indexPaths { imageLoader.prefetch(items[path.item].imageURL) }
}
func collectionView(_ cv: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
for path in indexPaths { imageLoader.cancel(items[path.item].imageURL) }
}
}Start downloads before cells appear. Cancel them when the user scrolls the other way.
6. Main thread blocked by other work
JSON parsing, database queries or reloadData() on every small change. Move heavy work off the main actor. Use a diffable data source with reconfigureItems instead of reloading everything.
SwiftUI lists
The same ideas apply:
- Use
ListorLazyVStack, notVStack, for long content. - Stable IDs in
ForEach. - Keep row
bodycheap. Pre-format text in the model. - Downsample images before display.
- Use the SwiftUI instrument in Xcode 26 to find long body updates.
Common follow-up questions
- What is the render server? A separate system process that composites layers on the GPU. A hitch can happen in your app (commit) or there (render).
- Does
cornerRadiuscause offscreen rendering? WithmasksToBoundsit can. It is cheaper than it used to be, so measure. Complex masks and shadows without a path are usually the bigger problem. - What is
shouldRasterize? It caches a layer as a bitmap. Good only for static content. If the content changes, it re-renders every time and gets slower. - How do you find hitches in production? MetricKit reports scroll hitch rates (
MXAnimationMetric).