How to Optimize SwiftUI ForEach Performance with Stable Identity

To optimize SwiftUI ForEach performance, ensure every data element provides a stable, unique identifier—such as a UUID or database primary key—so SwiftUI's diffing engine can reuse existing views instead of recreating them on every update.

SwiftUI's ForEach view drives the diffing engine that determines which views require recreation during state updates. When you optimize SwiftUI ForEach performance with stable identity, you prevent unnecessary view hierarchy churn, reduce memory pressure, and maintain smooth scrolling even with large datasets. These patterns are derived from the performance reference files found in the Dimillian/Skills open-source repository.

Why Stable Identity Controls Diffing Efficiency

When ForEach iterates over a collection, SwiftUI matches old and new items by their identifier to determine what changed. A stable, unique identifier allows the framework to reuse existing view instances efficiently. When identity changes for the same logical element—such as when using an unstable array index—SwiftUI treats it as a brand-new view, discarding the previous hierarchy and allocating fresh memory.

This identity churn causes janky scrolling, unnecessary recomputation, and higher memory pressure. According to the repository's swiftui-ui-patterns/references/performance.md file, maintaining stable identity is listed as a critical performance guardrail for SwiftUI applications.

Critical Anti-Patterns That Break Identity

Using Array Indices as Identifiers

Iterating with Array(...enumerated()) and using id: \.offset creates unstable identity. The offset changes whenever the collection reorders or filters, forcing a complete rebuild of the list even if the underlying data remains the same.

// AVOID: Unstable identity causing performance issues
ForEach(Array(items.enumerated()), id: \.offset) { index, item in
    ItemRow(item: item)
}

This pattern is explicitly discouraged in swiftui-performance-audit/references/code-smells.md under the "Unstable identity" section.

Applying id: .self to Mutable Structs

Using id: \.self on value types causes identity to change whenever any property mutates. Since self represents the entire struct instance, any modification breaks the stable identity contract, preventing view reuse.

// AVOID: Identity changes when any property updates
ForEach(items, id: \.self) { item in
    ItemRow(item: item)
}

Implementation Patterns for Stable Identity

Conforming to Identifiable with Stable IDs

Make your model types conform to Identifiable using a constant UUID or database primary key. This is the optimal pattern recommended in the performance guidelines.

struct FeedItem: Identifiable {
    let id: UUID                // stable, never changes
    let title: String
    let createdAt: Date
}

When the collection conforms to Identifiable, iterate directly without an explicit id parameter:

ForEach(feedItems) { item in
    FeedRow(item: item)
}

Supplying Explicit Key Paths for Legacy Models

For models that cannot adopt Identifiable, supply a key path to a stable property:

struct Photo {
    let url: URL          // stable across reloads
    let caption: String
}

ForEach(photos, id: \.url) { photo in
    PhotoRow(photo: photo)
}

The url uniquely identifies each photo, ensuring the grid can reuse view cells when the order changes.

Pre-filtering Collections Outside the View Body

Compute filtered or sorted collections in a view model or @State variable before rendering. As noted in swiftui-view-refactor/references/mv-patterns.md, keeping transformation logic out of the body property ensures ForEach receives a stable, ready-made array.

final class FeedViewModel: ObservableObject {
    @Published private(set) var displayedItems: [FeedItem] = []
    private var allItems: [FeedItem] = []

    func applyFilters(search: String) {
        displayedItems = allItems
            .filter { $0.title.localizedCaseInsensitiveContains(search) }
            .sorted { $0.createdAt > $1.createdAt }
    }
}

struct FeedScreen: View {
    @StateObject private var vm = FeedViewModel()

    var body: some View {
        List(vm.displayedItems) { item in
            FeedRow(item: item)
        }
    }
}

All heavy filtering and sorting happen in the view model, leaving body to simply render the already-stable displayedItems collection.

Enumerating with Stable Identifiers

When you must expose an index, wrap the enumeration in tuples where the first element derives from the stable identifier:

ForEach(Array(zip(feedItems, feedItems.indices)), id: \.0.id) { (item, index) in
    FeedRow(item: item, position: index)
}

This preserves the model's stable id for diffing while providing access to the positional index for display purposes.

Complete Code Examples

Identifiable Model with LazyVStack

For scroll-heavy feeds, wrap ForEach in a LazyVStack to defer view creation until cells approach the visible area. Combine this with stable identity for maximum performance.

struct ChatMessage: Identifiable {
    let id: UUID
    let author: String
    let body: String
}

struct ChatView: View {
    let messages: [ChatMessage]

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(messages) { message in
                    MessageRow(message: message)
                }
            }
        }
    }
}

The ChatMessage type supplies a stable UUID and the ForEach does not need an explicit id parameter.

Legacy Model Integration

When working with external data models that lack Identifiable conformance:

struct Photo {
    let url: URL
    let caption: String
}

struct PhotoGrid: View {
    let photos: [Photo]

    var body: some View {
        LazyVGrid(columns: [.adaptive(minimum: 100)]) {
            ForEach(photos, id: \.url) { photo in
                AsyncImage(url: photo.url) { image in
                    image.resizable()
                } placeholder: {
                    ProgressView()
                }
                .clipShape(RoundedRectangle(cornerRadius: 8))
            }
        }
    }
}

This pattern ensures that asynchronous image loading states persist correctly across data updates because each Photo maintains its identity via the stable url property.

Summary

  • Stable identifiers enable view reuse: Use UUIDs or database primary keys that remain constant across view updates.
  • Avoid indices as IDs: Array offsets change during filtering and sorting, causing complete view reconstruction.
  • Cache derived collections: Perform filtering, sorting, and searching in view models, not inside the body property.
  • Prefer Identifiable: Let SwiftUI infer identity automatically rather than supplying manual id parameters when possible.
  • Reference repository files: The Dimillian/Skills repository maintains detailed guidance in swiftui-ui-patterns/references/performance.md and swiftui-performance-audit/references/code-smells.md.

Frequently Asked Questions

What happens if I use array indices as ForEach identifiers?

SwiftUI treats each row as a new view whenever the collection order changes. This forces complete reconstruction of the view hierarchy, causing dropped frames during scrolling and breaking animations. The swiftui-performance-audit/references/code-smells.md file specifically flags id: \.offset patterns as high-impact performance anti-patterns.

Can I use id: .self with SwiftUI List views?

Only if the underlying type is immutable and value-based equality truly represents identity. For mutable structs, \.self causes identity churn whenever properties update. For value types that change frequently, use a stable property such as a UUID or timestamp instead.

How does stable identity affect animations in SwiftUI?

Transitions such as matchedGeometryEffect and implicit list animations rely on consistent identifiers to calculate start and end frames. When identity is unstable, SwiftUI cannot match views between states, causing animations to jump or fail entirely. Stable identity ensures the diffing engine can track elements through reordering operations.

Should I pre-filter data inside the ForEach closure?

No. Inline filtering such as ForEach(items.filter { ... }) creates a new array on every state change, potentially breaking identity if the filtering logic isn't deterministic. Pre-compute filtered collections in a view model or @State property, as recommended in swiftui-view-refactor/references/mv-patterns.md, to keep the body property lightweight and identity stable.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →