Question 7
SwiftUI lists, identity, and expensive body work
Scrolling stutters in a SwiftUI List with many rows. What causes unnecessary view invalidation, and how do you structure rows for performance?
Answer outline
Stutter in a long List almost always means SwiftUI is doing more work per scroll than it needs to. Three things fix most of it:
- 1.Stable identity: SwiftUI decides what changed by comparing view identity, so random IDs, fresh
UUID()values, or array indexes that shift on reorder make old rows look new. SwiftUI rebuilds them, drops their state, and scrolling jumps. - 2.Cheap rows:
bodycan run many times, so don't decode JSON, resize full images, create formatters, or filter large arrays inside it. Prepare data in the model, cache formatters, and downsample images before they reach the row. - 3.Narrow updates: give each row only the small piece of data it needs. If one parent state change invalidates the whole list, split rows into smaller views or pass simpler values so SwiftUI redraws less.
Start side effects from .task or .task(id:), or from an injected model, never from body. SwiftUI cancels .task when the view disappears and restarts .task(id:) when the id changes, which is usually a better fit than ad hoc onAppear network calls.
Principles
- Identity tells SwiftUI whether this is the same row or a brand new one.
bodydescribes the UI, and heavy work belongs in the model before the row is built.- Keep invalidation as local as you can, so one state change redraws one row instead of the whole list.
.task(id:)gives you cancellation on disappear and a restart when the id changes, whichonAppeardoesn't.
Use a real domain id that stays the same across reloads and reorders:
ForEach(items) { item in // Item.id is stable
RowView(item: item)
}
Follow-up angles
ForEach(items.indices)is fragile whenever the list can insert, delete, or reorder, because the index becomes the identity.- For images, use
AsyncImageor an image pipeline that downsamples, and never decode a full-size image inside a row view. - When guessing stops helping, profile with Instruments (the SwiftUI template or Time Profiler) and look for repeated
bodywork and broad invalidations.



