100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Swift

List Performance in SwiftUI

Learn how SwiftUI's List actually renders and recycles rows, and the concrete techniques for keeping scrolling smooth in large or complex data sets.

Lists & ScrollingIntermediate10 min readJul 8, 2026
Analogies

List Performance in SwiftUI

SwiftUI's List is built on UIKit's UITableView (or UICollectionView on newer OS versions) under the hood, which means it already benefits from cell reuse. But that doesn't make it immune to jank. Performance problems in List usually come from three places: identity churn that forces SwiftUI to throw away and rebuild rows instead of updating them, expensive work happening inside row bodies on every render, and eager evaluation of data that should be computed lazily. Understanding how List diffs its content using Identifiable or explicit id(_:) is the starting point for diagnosing slow lists.

🏏

Cricket analogy: A stadium reuses the same physical seats for every match like cell reuse, but if the scoreboard misidentifies which fan holds which ticket each over, ushers keep reseating everyone unnecessarily, causing chaos in the stands.

Stable Identity Is Everything

SwiftUI decides whether a row is 'the same view, just updated' or 'a brand new view' based on identity, not equality of content. If your ForEach uses array indices as IDs, or if your model's id changes when unrelated properties change (for example, a computed id derived from a mutable field), SwiftUI will misattribute rows, causing unnecessary teardown/rebuild cycles, lost @State, and visible flicker during scrolling or updates. Always prefer a stable, unique identifier — a UUID or a database primary key — that never changes for the lifetime of that logical item.

🏏

Cricket analogy: If a scorer tracks batsmen by position in the batting order instead of their name, a mid-innings retirement-hurt substitution confuses the whole scoreboard, wiping accumulated stats instead of just updating the new batsman's line.

swift
struct Message: Identifiable {
    let id: UUID
    var text: String
    var isRead: Bool
}

struct InboxView: View {
    @State private var messages: [Message] = MessageStore.sampleMessages

    var body: some View {
        List(messages) { message in
            MessageRow(message: message)
        }
        .listStyle(.plain)
    }
}

struct MessageRow: View {
    let message: Message

    var body: some View {
        HStack {
            Circle()
                .fill(message.isRead ? Color.clear : Color.blue)
                .frame(width: 8, height: 8)
            Text(message.text)
                .lineLimit(2)
            Spacer()
        }
    }
}

Keep Row Bodies Cheap

Every property access inside a row's body re-executes on each render pass SwiftUI performs for that row, so expensive formatting, image decoding, or date math should be precomputed and stored on the model, not recalculated inline. Extracting each row into its own small View struct (as MessageRow above) also matters: it lets SwiftUI diff and update that subview independently, rather than re-evaluating a monolithic closure body for the entire list every time any single row changes.

🏏

Cricket analogy: Recalculating a batsman's full career strike rate from raw ball-by-ball data every time the scoreboard refreshes is wasteful; store the running average and just update it, and give each batsman's tile its own mini-scoreboard so only that tile redraws.

A common pitfall is loading and decoding images synchronously inside a row body, or performing a full-collection filter/sort computation directly in the List's content closure. Both of these run repeatedly as rows scroll on and off screen, causing dropped frames. Use AsyncImage or a caching image loader, and precompute filtered/sorted arrays outside of body.

LazyVStack vs. List

List gives you native cell recycling, section headers, swipe actions, and platform-correct styling for free, and for most data-driven, scrollable content it is the right default. A ScrollView with LazyVStack is more appropriate when you need full control over the visual layout — for example a custom grid-like feed, or when you don't want List's built-in insets, separators, and selection chrome. Both are lazy: they only materialize views for rows near the visible viewport, so the real performance lever is what happens inside each row, not which container you pick.

🏏

Cricket analogy: A standard stadium (List) gives you built-in turnstiles, seating charts, and stewarding for free; a pop-up ground on a village field (LazyVStack) gives full control over layout for a custom format, but either way the real bottleneck is how fast each gate processes a fan, not which venue you pick.

Instruments' SwiftUI and Core Animation templates can show you exactly how many times a row body is evaluated per second while scrolling. If you see a row re-rendering far more often than the data actually changes, suspect identity issues or a piece of @State/@ObservedObject scoped too broadly.

  • List reuses underlying platform cells, but SwiftUI still diffs rows by identity, so unstable or duplicate IDs cause unnecessary rebuilds.
  • Prefer Identifiable models with a truly stable id (UUID or database key) over array-index-based identity.
  • Extract row content into dedicated View structs so SwiftUI can diff and update rows independently.
  • Avoid doing expensive work (image decoding, filtering, formatting) inside a row's body; precompute and store it on the model.
  • List and LazyVStack are both lazy about materializing offscreen content; choose based on desired chrome, not raw performance.
  • Use Instruments to measure actual row re-render frequency rather than guessing at the source of jank.

Practice what you learned

Was this page helpful?

Topics covered

#Swift#IOSWithSwiftUIStudyNotes#MobileDevelopment#ListPerformanceInSwiftUI#List#Performance#SwiftUI#Stable#DataStructures#StudyNotes#SkillVeris