SwiftUI 'List' performance issue on macOS

Hi, When using SwiftUI ‘List’ with a large number of elements (4000+), I noticed a significant performance issue if extracting the views inside the ‘ForEach’ block into their own subview class. It affects scrolling performance, and using the scroll handle in the scrollbar causes stutters and beachballs. This seems to happen on macOS only ... the same project works fine on iOS.

Here's an example of what I mean:

List (selection: $multiSelectedContacts) {
      ForEach(items) { item in
                    
      // 1. this subview is the problem ... replace it with the contents of the subview, and it works fine
      PlainContentItemView(item: item)

      // 2. Uncomment this part for it to work fine (and comment out PlainContentItemView above)
      /*HStack {
          if let timestamp = item.timestamp, let itemNumber = item.itemNumber {
              Text("\(itemNumber) - \(timestamp, formatter: itemFormatter)")
            }
         }*/
     }
 }

struct PlainContentItemView: View {
    let item: Item
    var body: some View {
        
        HStack {
            if let timestamp = item.timestamp, let itemNumber = item.itemNumber {
                Text("\(itemNumber) - \(timestamp, formatter: itemFormatter)")
            }
        }
    }
}

Item is a NSManagedObject subclass, and conforms to Identifiable by using the objectID string value.

With this, scrolling up and down using the scrolling handle, causes stuttering scrolling and can beachball on my machine (MacBook Pro M1).

If I comment out the ‘PlainContentItemView’ and just use the HStack directly (which is what was extracted to ‘PlainContentItemView’), the performance noticeably improves, and I can scroll up and down smoothly.

Is this just a bug with SwiftUI, and/or can I do something to improve this?

Hey!

This is expected but let me explain why this is happening and how you can stay on the fast path.

SwiftUI needs to know the total number of rows in the List upfront. This can be done very efficiently if SwiftUI knows statically that each View returned by ForEach returns a constant number of children. if,other conditional construct or AnyView make this number dynamic. This requires SwiftUI to eagerly iterated over all elements, create those views and compute how many rows this ForEach actually produces. And it needs to do this on every change.

If the number of rows per view returned by ForEach is instead know statically, e.g. if conditionals are wrapped in an HStack or have only unconditional concrete views, SwiftUI can simply multiple this static number with count of data given to ForEach.

SwiftUI needs to know the number of rows for multiple reasons but the scroll indicator is one example. This is very similar to UICollectionView or NSTableView.

I hope this helps to understand the performances difference you see with and without an HStack.

Hi. This doesn't explain why the subview version works fine on iOS, but is very slow on macOS. Also, the branching is the same in both cases ... the conditional is wrapped in an HStack in both cases.

Also, I filed a feedback a few months ago regarding this issue: FB15645433

Just to follow up on this (as it's still an issue in macOS 15.4), based on your feedback, the following should be equivalent in performance (but they're not):

// 1. the 'fast' version
struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    
    @FetchRequest(
        fetchRequest: Item.myFetchRequest(),
        animation: .default)
    
    private var items: FetchedResults<Item>
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(items) { item in
                    
                    Text("\(item.itemNumber ?? "None")")
                }
            }
        }
    }
}
// 2. The 'slow' version
struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    
    @FetchRequest(
        fetchRequest: Item.myFetchRequest(),
        animation: .default)
    
    private var items: FetchedResults<Item>
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(items) { item in
                    ContentItemView(item: item)
                    //Text("\(item.itemNumber ?? "None")")
                }
            }
        }
    }
}

// extracted subview
struct ContentItemView: View {
    
    let item: Item
    
    var body: some View {
        
        Text("\(item.itemNumber ?? "None")") 
    }
}

If you add 5k rows here, and try to drag the window scrubber up and down to scroll through the list, you can see the performance problems in the 2nd version. The number of subviews in each row are equal in both implementations.

Your explanation makes sense for iOS but not for macOS, where there's some performance problem under the hood.

SwiftUI 'List' performance issue on macOS
 
 
Q