# How to Optimize SwiftUI ForEach Performance with Stable Identity

> Boost SwiftUI ForEach performance by implementing stable identity. Learn how unique identifiers allow SwiftUI to reuse views efficiently, enhancing your app's speed and responsiveness.

- Repository: [Thomas Ricouard/Skills](https://github.com/Dimillian/Skills)
- Tags: performance
- Published: 2026-04-01

---

**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`](https://github.com/Dimillian/Skills/blob/main/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.

```swift
// 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`](https://github.com/Dimillian/Skills/blob/main/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.

```swift
// 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.

```swift
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:

```swift
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:

```swift
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`](https://github.com/Dimillian/Skills/blob/main/swiftui-view-refactor/references/mv-patterns.md), keeping transformation logic out of the `body` property ensures `ForEach` receives a stable, ready-made array.

```swift
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:

```swift
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.

```swift
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:

```swift
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`](https://github.com/Dimillian/Skills/blob/main/swiftui-ui-patterns/references/performance.md) and [`swiftui-performance-audit/references/code-smells.md`](https://github.com/Dimillian/Skills/blob/main/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`](https://github.com/Dimillian/Skills/blob/main/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`](https://github.com/Dimillian/Skills/blob/main/swiftui-view-refactor/references/mv-patterns.md), to keep the `body` property lightweight and identity stable.