Slow rendering List backed by SwiftData @Query

Hello, I've a question about performance when trying to render lots of items coming from SwiftData via a @Query on a SwiftUI List. Here's my setup:

// Item.swift:

@Model final class Item: Identifiable {
  var timestamp: Date
  var isOptionA: Bool
  init() {
    self.timestamp = Date()
    self.isOptionA = Bool.random()
  }
}

// Menu.swift

enum Menu: String, CaseIterable, Hashable, Identifiable {
  var id: String { rawValue }
  case optionA
  case optionB
  case all
  var predicate: Predicate<Item> {
    switch self {
    case .optionA: return #Predicate { $0.isOptionA }
    case .optionB: return #Predicate { !$0.isOptionA }
    case .all: return #Predicate { _ in true }
    }
  }
}

// SlowData.swift

@main
struct SlowDataApp: App {
  var sharedModelContainer: ModelContainer = {
    let schema = Schema([Item.self])
    let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
    return try! ModelContainer(for: schema, configurations: [modelConfiguration])
  }()
  
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
    .modelContainer(sharedModelContainer)
  }
}

// ContentView.swift

struct ContentView: View {
  @Environment(\.modelContext) private var modelContext
  @State var selection: Menu? = .optionA
  var body: some View {
    NavigationSplitView {
      List(Menu.allCases, selection: $selection) { menu in
        Text(menu.rawValue).tag(menu)
      }
    } detail: {
      DemoListView(selectedMenu: $selection)
    }.onAppear {
      // Do this just once
//      (0..<15_000).forEach { index in
//        let item = Item()
//        modelContext.insert(item)
//      }
    }
  }
}

// DemoListView.swift

struct DemoListView: View {
  @Binding var selectedMenu: Menu?
  @Query private var items: [Item]
  init(selectedMenu: Binding<Menu?>) {
    self._selectedMenu = selectedMenu
    self._items = Query(filter: selectedMenu.wrappedValue?.predicate,
                        sort: \.timestamp)
  }
  var body: some View {
    // Option 1: touching `items` = slow!
    List(items) { item in
      Text(item.timestamp.description)
    }
    
    // Option 2: Not touching `items` = fast!
//    List {
//      Text("Not accessing `items` here")
//    }
    .navigationTitle(selectedMenu?.rawValue ?? "N/A")
  }
}

When I use Option 1 on DemoListView, there's a noticeable delay on the navigation. If I use Option 2, there's none. This happens both on Debug builds and Release builds, just FYI because on Xcode 16 Debug builds seem to be slower than expected: https://indieweb.social/@curtclifton/113273571392595819

I've profiled it and the SwiftData fetches seem blazing fast, the Hang occurs when accessing the items property from the List. Is there anything I'm overlooking or it's just as fast as it can be right now?

After more digging, it looks like List is doing the correct thing, but @Query seems to be loading all data up-front.

I've enabled the SQL debug logging and I see the following when I tap on the All menu item:

CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZISOPTIONA FROM ZITEM t0 ORDER BY t0.Z_PK
CoreData: annotation: sql connection fetch time: 0.0025s
CoreData: annotation: fetch using NSSQLiteStatement <0x60000622dd60> on entity 'Item' with sql text 'SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZISOPTIONA FROM ZITEM t0 ORDER BY t0.Z_PK' returned 15000 rows

But then if I add a print statement on the init of what would be the row view of each List item, it only calls the init for as many times as rows fit on-screen.

So, my final question is: If the fetch to the database is pretty much instant, and List only loads the cells that are visible on-screen, why there's such a massive hang? If instead of using @Query I simply pass 15_000 Items, there's no hang. But the fetch seems extremely fast, so I'm really confused at where's the problem.

Gist with the code: https://gist.github.com/xmollv/e682ad6c22ac0bfd4b410e9fdb6db5c4

"So, my final question is: If the fetch to the database is pretty much instant, and List only loads the cells that are visible on-screen, why there's such a massive hang? "

The time shown as "sql connection fetch time" in the SQLDebug log is the time Core Data uses to run the SQL statement, which is quite different from the time SwiftData uses to retrieve the data from the data store to the item array.

To show the difference, I tweaked your code in the following way:

struct DemoListView: View {
    @Binding var selectedMenu: Menu?
    @Query private var items: [Item]
    
    var itemsWithLog: [Item] {
        let startTime = Date.now
        let tempItems = items
        print("Time taken to fetch: \(Date.now.timeIntervalSince(startTime))")
        return tempItems
    }

    init(selectedMenu: Binding<Menu?>) {
        self._selectedMenu = selectedMenu
        self._items = Query(filter: selectedMenu.wrappedValue?.predicate)
    }
    
    var body: some View {
        List(itemsWithLog) { item in
            ListRowView(item: item)
        }
        .navigationTitle(selectedMenu?.rawValue ?? "N/A")
    }
}

With this, when running your app on my iPhone 16 Pro Max simulator and selecting the all row, I got the following output:

CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZISOPTIONA FROM ZITEM t0 ORDER BY t0.Z_PK
CoreData: annotation: sql connection fetch time: 0.0059s
CoreData: annotation: fetch using NSSQLiteStatement <0x60000236ed00> on entity 'Item' with sql text 'SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZISOPTIONA FROM ZITEM t0 ORDER BY t0.Z_PK' returned 15000 rows
CoreData: annotation: total fetch execution time: 0.0062s for 15000 rows.
Time taken to fetch: 0.37052595615386963

As you can see, the time SwiftData used to retrieve the data was 0.37s, which was significantly larger than 0.0062s, the "total fetch execution time".

By time profiling the app, I confirmed that the 0.37s was pretty much the hang time shown in the Instruments trace.

So the app actually works correctly, except that the performance of SwiftData isn't quite as good as you would expect. If that is an issue for your app, I’d suggest that you file a feedback report – If you do so, please share your report ID here for folks to track.

In a situation like this, where the data set can eventually grow to pretty large, which can trigger a performance issue, I'd probably consider designing the app in a way that presents a smaller data set, like adding some filters or implementing pagination.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Hey @DTS Engineer, thanks for your response! I do believe this should work as expected (no hang whatsoever) without the need for pagination. On UIKit + CoreData you don't need pagination, using an NSFetchedResultsController allows you to display as many items as you wish on a UITableView/UICollectionView without any hangs whatsoever.

I've filed FB15507372 and put up the project for anyone to download and run here so it's easily reproducible: https://github.com/xmollv/SlowData

Slow rendering List backed by SwiftData @Query
 
 
Q