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

SwiftUI 'List' performance issue on macOS
 
 
Q