I’ve been trying to resolve some crippling performance issues in a SwiftUI app for macOS with a few thousand objects.
Unfortunately, the documentation of Realm’s Property Wrappers is not very detailed and much of SwiftUI’s behavior remains opaque, so I’d appreciate if someone could confirm my findings or suggest better ways to improve performance.
- On macOS, SwiftUI’s
List
is not lazy-loading. When usingForEach
on@ObservedResults
and populating aSwiftUI.List
, every single row’s view is initially created. - By default, ANY change to ANY object in
@ObservedResults
will invalidate ALL views in the correspondingForEach
. -
ForEach
is smart enough to only compute the bodies of views that are currently visible. Views that later come into view are computed lazily. - By specifying
keyPaths
in@ObservedResults
, invalidation only occurs if the specified properties change (but still affect ALL items of the collection, even if only a single object has been modified). - When using
NavigationLink
, SwiftUI will initialize each destination view EVERY SINGLE TIME when the NavigationLink is computed. This will lead to a potentially huge number of initializations of destination views and therefore any contained@ObservedRealmObject
property wrapper. - Specifying the
id
parameter inForEach
doesn’t have any impact, as Realm objects already need to beIdentifiable
to be used in@ObservedResults
Workarounds I’ve found:
- Use the
keyPaths
parameter of@ObservedResults
to limit refreshing to the relevant properties (e.g. the ones that are actually shown). Optionally, limitkeyPath
to a bogus property to have@ObservedResults
only trigger for changes to the collection (add/remove). - Keep views as lean as possible (e.g. if a row only requires a String with a name, pass only that property and not the whole object).
- Only use
@ObservedRealmObject
for situations where the view is visible (e.g. don’t use it in hundreds of row views in a list). - Conform to
Equatable
in views that rely only on a@ObservedRealmObject
for content updates (to prevent them from being refreshed from outside). ImplementEquatable
by comparing the identity of the@ObservedRealmObject
. - Debounce text fields that are bound to properties of a
@ObservedRealmObject
(not quite sure what’s the best way to do that yet). - When using
NavigationLink
with a destination view that has a@ObservedRealmObject
, passnil
as destination if the item is not currently selected
It seems to me that there are quite a few design decisions in SwiftUI that currently make it extremely hard to integrate Realm in a way that is both simple and performant. I hope Apple will improve on this by implementing fine-grained invalidation and better-performing UI elements for macOS.
The following article helped me better understand what’s going on in SwiftUI: Understanding how and when SwiftUI decides to redraw views – Donny Wals
Simple example demonstration the performance issues
// SwiftUI app for macOS
import SwiftUI
import RealmSwift // 10.25.0
@main
struct TestApp: SwiftUI.App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@State var selection: UInt64? = nil
@ObservedResults(Car.self) var cars
var body: some View {
NavigationView {
List {
ForEach(cars) { car in
let _ = print("Computing `NavigationLink` for `\(car.name)`")
NavigationLink(destination: CarDetailView(car: car), tag: car.id, selection: $selection) {
CarCell(car: car)
}
}
}
Text("Choose a car")
.foregroundColor(.secondary)
}
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: add10KCars) {
Label("Add 10 000 cars", systemImage: "plus")
}
}
}
}
func add10KCars() {
let numberOfCars = cars.count
let realm = try! Realm()
try! realm.write {
for index in 0..<10_000 {
realm.add(Car("Car \(numberOfCars+index+1)"))
}
}
}
}
struct CarDetailView : View {
@ObservedRealmObject var car:Car
init(car:Car) {
print("Initializing `CarDetailView` for `\(car.name)`")
self.car = car
}
var body: some View {
let _ = print("Computing `CarDetailView` for `\(car.name)`")
TextField("Name", text: $car.name)
.padding()
TextField("Model", text: $car.model)
.padding()
}
}
struct CarCell : View {
@ObservedRealmObject var car:Car
init(car:Car)
{
print("Initializing `CarCell` for `\(car.name)`")
self.car = car
}
var body: some View {
let _ = print("Computing `CarCell` for `\(car.name)`")
Text(car.name)
}
}
class Car : Object, ObjectKeyIdentifiable {
@Persisted var name = "Car \(Date.now.timeIntervalSince1970.description)"
@Persisted var model = ""
convenience init(_ name:String) {
self.init()
self.name = name
}
}
With some performance enhancements (but still not great)
// SwiftUI app for macOS
import SwiftUI
import RealmSwift // 10.25.0
@main
struct TestApp: SwiftUI.App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@State var selection: UInt64? = nil
@ObservedResults(Car.self, keyPaths: ["name"]) var cars
var body: some View {
NavigationView {
List {
ForEach(cars) { car in
let _ = print("Computing `NavigationLink` for `\(car.name)`")
NavigationLink(destination: selection == car.id ? CarDetailView(car: car) : nil, tag: car.id, selection: $selection) {
CarCell(name: car.name)
}
}
}
Text("Choose a car")
.foregroundColor(.secondary)
}
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: add10KCars) {
Label("Add 10 000 cars", systemImage: "plus")
}
}
}
}
func add10KCars() {
let numberOfCars = cars.count
let realm = try! Realm()
try! realm.write {
for index in 0..<10_000 {
realm.add(Car("Car \(numberOfCars+index+1)"))
}
}
}
}
struct CarDetailView : View, Equatable {
@ObservedRealmObject var car:Car
init(car:Car) {
print("Initializing `CarDetailView` for `\(car.name)`")
self.car = car
}
var body: some View {
let _ = print("Computing `CarDetailView` for `\(car.name)`")
TextField("Name", text: $car.name)
.padding()
TextField("Model", text: $car.model)
.padding()
}
static func == (lhs: CarDetailView, rhs: CarDetailView) -> Bool {
return lhs.car.id == rhs.car.id
}
}
struct CarCell : View {
let name:String
init(name:String) {
print("Initializing `CarCell` for `\(name)`")
self.name = name
}
var body: some View {
let _ = print("Computing `CarCell` for `\(name)`")
Text(name)
}
}
class Car : Object, ObjectKeyIdentifiable {
@Persisted var name = "Car \(Date.now.timeIntervalSince1970.description)"
@Persisted var model = ""
convenience init(_ name:String) {
self.init()
self.name = name
}
}