SwiftData-Performance ist ein Speicherproblem
Das SwiftData-Group-Lab auf der WWDC 2026 war mit den Leuten besetzt, denen die Schichten unterhalb des Frameworks gehören, darunter der Engineer, der SQLite über Apples Plattformen hinweg pflegt, und der Manager von Core Data und SwiftData. Der rote Faden ihrer Antworten war eine nützliche Korrektur dessen, wie die meisten Entwickler nach Performance greifen: Sobald SwiftData in Ihrer App steckt, ist das Teure die I/O, nicht Ihr Swift-Code, und die Gewinne kommen daher, weniger zu lesen und die Storage-Engine zu verstehen, statt Nebenläufigkeit hinzuzufügen. Das meiste im Folgenden stützt sich auf Apples Dokumentation und die von SQLite; wo eine Aussage die technische Argumentation des Labs ist und keine dokumentierte Tatsache, ist sie als solche gekennzeichnet.
Das SwiftData-Group-Lab auf der WWDC 2026.
TL;DR
- Der SQLite-Store von SwiftData nutzt standardmäßig write-ahead logging (WAL), was bedeutet, dass mehrere Leser nebenläufig mit einem einzigen Schreiber laufen. Es ist kein Reader/Writer-Lock — eine Unterscheidung, bei der Entwickler dem Lab zufolge regelmäßig danebenliegen.23
- Lesen, ohne Objekte zu materialisieren:
fetchCount(_:)liefert eine Trefferanzahl undfetchIdentifiers(_:)liefert[PersistentIdentifier], beide ohne Modelle zu hydratisieren. Kombinieren Sie sie mit der History-Beobachtung, um zu entscheiden, ob ein Refresh überhaupt nötig ist.45 @Model-Objekte sind nichtSendableund sollten nicht dazu gezwungen werden. Um eine Actor-Grenze zu überqueren, geben Sie denPersistentIdentifier(derSendableist) zusammen mit allen extrahierten Werten weiter und holen Sie das Objekt dann im Ziel-Context erneut.6- SwiftData hat kein Äquivalent zu Core Datas in SQL ausgeführten Aggregat-Abfragen (sum, average, min, max). Der Notausgang ist Koexistenz: Betreiben Sie einen Core-Data-Stack gegen dieselbe Store-Datei und lassen Sie ihn das Aggregat berechnen.8
- Die Performance-Botschaft des Labs: Profilen Sie, um herauszufinden, warum SwiftUI erneut geladen hat, bevor Sie annehmen, die Datenbank sei langsam, denn übermäßige View-Invalidierung sieht aus wie ein I/O-Problem, ist es aber nicht.110
WAL: nebenläufige Leser, ein Schreiber, kein Lock
Die mit Abstand nützlichste Korrektur aus dem Lab betrifft die Nebenläufigkeit auf der Storage-Ebene. SwiftData baut auf dem Core-Data-SQLite-Store auf, und dieser Store nutzt seit iOS 7 standardmäßig write-ahead logging.3 Unter WAL gilt, wie es SQLites Dokumentation formuliert: „WAL provides more concurrency as readers do not block writers and a writer does not block readers. Reading and writing can proceed concurrently.”2 Es gibt nach wie vor zu jedem Zeitpunkt genau einen Schreiber, aber das mentale Modell eines Mutex, der jeglichen Datenbankzugriff serialisiert, ist falsch: Ihre Lesezugriffe müssen nicht hinter dem Schreibvorgang warten.
Die Formulierung des Labs, sinngemäß aus der Aufnahme wiedergegeben, lautete, dass Leute die Einzel-Schreiber-Regel wie einen Reader/Writer-Lock behandeln und um eine Einschränkung herum architektonisch planen, die gar nicht existiert.1 Das zutreffende Modell ist das WAL-Modell: Planen Sie für viele nebenläufige Lesevorgänge und einen serialisierten Schreibvorgang, nicht für globalen Ausschluss.
Weniger lesen: zählen und identifizieren ohne Hydratisieren
Wenn I/O die Kosten verursacht, ist der wirkungsvollste Schritt, das Laden von Objekten einzustellen, die Sie nicht brauchen. SwiftData bietet dafür zwei Primitive, beide in der API verifiziert:
fetchCount(_:) auf ModelContext nimmt einen FetchDescriptor entgegen und liefert die Anzahl der passenden Modelle als Int, ohne eines davon zu instanziieren.4 Wenn Sie eine Anzahl für ein Badge oder einen Section-Header benötigen, ist das strikt günstiger, als zu fetchen und .count aufzurufen.
fetchIdentifiers(_:) liefert [PersistentIdentifier] für einen Descriptor, wiederum ohne die Modelle zu materialisieren, und eine Überladung fetchIdentifiers(_:batchSize:) verarbeitet die Arbeit in Batches.5 Der vom Lab vorgeschlagene Einsatz, sinngemäß wiedergegeben, kombiniert dies mit der History-Beobachtung: Wenn eine Änderung eintrifft, holen Sie die betroffenen Identifiers und vergleichen Sie sie mit dem, was Ihr View tatsächlich anzeigt, bevor Sie entscheiden, ob überhaupt etwas neu geladen wird.1 Die History- und Beobachtungs-APIs selbst werden in SwiftDatas iOS-27-Beobachtung und -History behandelt; fetchIdentifiers ist der leichtgewichtige Lesevorgang, der sie effizient macht. Der Beobachtungstyp, nach dem Sie außerhalb von SwiftUI greifen sollten, ist ResultsObserver, der auf Swift Observation basierende Observer, der für die Releases von 2027 eingeführt wurde und dieselben Primitive wie @Query unterstützt, einschließlich der Key-Path-Sektionierung über sectionBy:.9
Die Sendable-Grenze ist real, und der Modellgraph überquert sie nicht
SwiftData-Modelle sind Referenztypen, die innerhalb ihres Contexts in einen Graphen eingehängt sind, und sie sind nicht Sendable. Das Lab war unverblümt darin, dass man sie nicht sinnvoll dazu zwingen kann, weil der Graph nicht thread-sicher ist und ihn auf einem anderen Actor teilweise zu hydratisieren zu Problemen führt.1 Das unterstützte Muster verwendet PersistentIdentifier, der Sendable, Hashable und Codable ist, als die Identität, die Sie über Grenzen hinweg bewegen.6 Extrahieren Sie die benötigten Werte in ein Struct, hängen Sie den PersistentIdentifier an, übergeben Sie das an den anderen Actor und holen Sie das Modell im Ziel-Context erneut, wenn Sie das lebende Objekt brauchen.
Eine Präzisierung, die man sich merken sollte: Apple weist darauf hin, dass ein dekodierter PersistentIdentifier und einer, den der Standard-Store erzeugt hat, nicht immer als gleichwertig gelten — behandeln Sie den Identifier also als stabiles Context-übergreifendes Handle, statt anzunehmen, dass eine dekodierte Kopie einem lebenden Objekt gleicht.6
Dieselbe Disziplin — Identität statt Graph — zeigt sich auch über Prozessgrenzen hinweg. Wenn Sie einen Store in eine App-Group verschieben, um ihn mit einem Widget oder einer Extension zu teilen, kopiert die Standardkonfiguration den vorhandenen Store für Sie in den App-Group-Container; bei einer eigenen Store-URL verwalten Sie den Speicherort selbst.7 So oder so koordinieren sich die Prozesse über den Store und seine Identifiers, nicht durch das Weiterreichen lebender Objekte.
Die Aggregat-Lücke und der Core-Data-Notausgang
Eine echte Einschränkung, die das Lab benannte: SwiftData hat kein Äquivalent zu Core Datas auf NSExpression basierenden Aggregat-Abfragen — jenen, die sum, average, min und max bis hinunter in SQLite drücken, sodass die Datenbank sie berechnet, ohne Zeilen zu laden.8 In SwiftData würden Sie die Zeilen fetchen und im Arbeitsspeicher reduzieren, was bei einer großen Tabelle den Zweck zunichtemacht. Für min oder max können Sie mit einem Sort-Descriptor und einem Fetch-Limit von eins fetchen; für echte Aggregate verwies das Lab auf Koexistenz.
Koexistenz ist, wie Apple es auf der WWDC 2023 formulierte, „two completely separate persistent stacks, one Core Data stack and one SwiftData stack, talking to the same persistent store.”8 Beide Stacks zeigen auf dieselbe Store-URL, und weil SwiftData die Persistent-History-Verfolgung automatisch aktiviert, muss die Core-Data-Seite ebenfalls NSPersistentHistoryTrackingKey aktivieren, sonst öffnet sich der Store schreibgeschützt.8 Mit dieser Voraussetzung können Sie das in SQL ausgeführte Aggregat über Core Data gegen genau die Datei laufen lassen, die SwiftData gehört. Das ist mehr Maschinerie, als die meisten Apps brauchen, aber es ist der dokumentierte Weg, wenn Sie wirklich eine Aggregation auf Datenbankseite benötigen.
Profilen Sie die Invalidierung, nicht nur die Datenbank
Die praktischste Performance-Anleitung des Labs, sinngemäß wiedergegeben, lautete, dass die scheinbaren I/O-Kosten einer SwiftData-App oft ein als solches getarntes SwiftUI-Invalidierungsproblem sind: Ein View, der zu oft invalidiert, lädt erneut, und ein Profiler zeigt den erneuten Fetch als Datenbankzeit an, während der eigentliche Fehler darin liegt, dass der View überhaupt nicht hätte aktualisiert werden dürfen.1 Die Lösung ist dieselbe View-Isolations-Disziplin, die bei jedem SwiftUI-Performance-Problem hilft, behandelt in SwiftUI-Performance und -Interop: Zerlegen Sie große Views in kleinere mit engeren Abhängigkeiten und reichen Sie bereits gefetchte Modelle nach unten weiter, damit die Query nicht erneut läuft.
Das Tooling unterstützt diese Sichtweise. Instruments liefert ein SwiftUI-Template, das das SwiftUI-Instrument zusammen mit den Hangs- und Hitches-Instrumenten bündelt, ein File-Activity-Template, dessen Reads-and-Writes-Instrument den tatsächlichen Festplattenverkehr zeigt (nur auf dem Gerät, nicht im Simulator), und das Core-Data-Template mit seinem Data-Persistence-Instrument, das Faults, Fetches und Saves meldet.10 Wenn Sie die SwiftUI- und die Persistenz-Ansichten zusammen laufen lassen, erfahren Sie, ob ein erneuter Fetch ein echter Lesevorgang war oder ein redundanter, ausgelöst durch übermäßige Invalidierung.
Eine Warnung, die das Lab zum Benchmarking aussprach, sinngemäß wiedergegeben: Es gibt Caches auf allen Ebenen — den SQLite-Page-Cache, den OS-Datei-Cache und den Storage-Controller —, sodass ein „schneller” Lauf eher ein Cache-Treffer als eine echte Verbesserung sein kann. Messen Sie gegen einen realistisch großen Datensatz und nutzen Sie das File-Activity-Instrument, um zu bestätigen, dass tatsächlich I/O stattgefunden hat.1
Zum Hinzufügen von Nebenläufigkeit
Die stärkste Meinung des Labs — und der Teil, der als technische Argumentation und nicht als dokumentierte Tatsache zu behandeln ist — war eine Warnung davor, nach Nebenläufigkeit als Performance-Lösung zu greifen. Die Engineers beschrieben SwiftDatas Connection-Pooling als bewusst begrenzt und argumentierten, dass man jenseits einer kleinen Anzahl nebenläufiger Operationen an die Obergrenze der Storage-Hardware stößt, sodass mehr Contexts abnehmende Erträge bringen — zum Preis von mehr Arbeitsspeicher und mehr I/O.1 Apple dokumentiert kein konkretes Nebenläufigkeitslimit, übernehmen Sie also von niemandem eine harte Zahl, auch nicht aus diesem Beitrag. Die vertretbare Erkenntnis ist die richtungsweisende: Auf einem Gerät mit Flash-Speicher ist es kein verlässlicher Weg, schneller zu werden, immer mehr nebenläufige Schreiber aufzutürmen — und das WAL-Modell schenkt Ihnen nebenläufige Lesevorgänge ohnehin gratis.
Was man daraus mitnimmt
Das Lab rahmt SwiftData-Performance um die Storage-Engine neu. Die verifizierten Hebel sind konkret: Setzen Sie auf WALs nebenläufige Lesevorgänge, statt einen Lock zu fürchten, nutzen Sie fetchCount und fetchIdentifiers, um das Hydratisieren von Objekten zu vermeiden, bewegen Sie PersistentIdentifier über Actors hinweg statt des Modellgraphen und greifen Sie zur Core-Data-Koexistenz, wenn Sie ein echtes Aggregat brauchen. Die Profiling-Disziplin besteht darin, zu bestätigen, dass eine I/O-Last real ist, bevor Sie die Datenbank optimieren, denn der Übeltäter ist oft ein View, der sich aktualisiert hat, obwohl er es nicht hätte tun sollen.
FAQ
Sperrt SwiftData die Datenbank während Schreibvorgängen?
Nicht im Sinne eines Reader/Writer-Locks. Der Store nutzt SQLite write-ahead logging, das es mehreren Lesern erlaubt, nebenläufig mit einem einzigen Schreiber zu laufen; Lesevorgänge blockieren den Schreiber nicht und der Schreiber blockiert die Lesevorgänge nicht.23 Es gibt zu jedem Zeitpunkt einen Schreiber, aber Lesevorgänge laufen daneben weiter.
Wie zähle oder prüfe ich Datensätze, ohne sie zu laden?
Verwenden Sie ModelContext.fetchCount(_:) für eine Trefferanzahl und ModelContext.fetchIdentifiers(_:) für [PersistentIdentifier]-Werte, von denen keiner Modellobjekte materialisiert.45 Kombinieren Sie fetchIdentifiers mit der History-Beobachtung, um vor dem erneuten Laden zu entscheiden, ob eine Änderung das, was Ihr View anzeigt, tatsächlich betrifft.
Wie übergebe ich ein SwiftData-Objekt an einen anderen Actor?
Sie übergeben das Objekt nicht. @Model-Typen sind nicht Sendable. Übergeben Sie den PersistentIdentifier (der Sendable ist) zusammen mit allen extrahierten Werten und holen Sie das Modell dann im Ziel-Context erneut.6 Vermeiden Sie es, den lebenden Modellgraphen über die Grenze zu reichen.
Kann SwiftData sum/average/min/max in der Datenbank ausführen?
Nein. SwiftData hat kein Äquivalent zu Core Datas auf NSExpression basierenden, in SQL ausgeführten Aggregaten.8 Für min/max fetchen Sie mit einem Sort und einem Fetch-Limit von eins; für echte Aggregate betreiben Sie einen Core-Data-Stack gegen dieselbe Store-Datei (Koexistenz), was voraussetzt, dass die Store-URL übereinstimmt und die Persistent-History-Verfolgung auf der Core-Data-Seite aktiviert ist.8
Die SwiftData-Spur in diesem Blog behandelt die Schema- und Migrations-Disziplin in Schema-Disziplin und dem Migrations-Leitfaden sowie die iOS-27-Beobachtungs- und -History-APIs in Beobachtung und History. Dieser Beitrag fügt die Performance- und Storage-Schicht hinzu. Der vollständige Serien-Hub ist die Apple-Ecosystem-Serie.
References
-
Apple, WWDC 2026 session 8017, SwiftData Group Lab. Sinngemäß aus einer lokal transkribierten Aufnahme wiedergegeben; Apple veröffentlicht keine offiziellen Untertitel für die Labs, daher ist der Wortlaut hier eine Paraphrase, kein Zitat, und die genaue Formulierung ist unverifiziert. Quelle für die Einordnung des Reader/Writer-Lock-Missverständnisses, den Vorschlag,
fetchIdentifiersplus History zur Refresh-Steuerung zu nutzen, die Transfer-Anleitung für nicht-Sendable@Model-Objekte, den Punkt, dass View-Invalidierung sich als I/O tarnt, die „caches all the way down”-Benchmarking-Warnung und die Position zu Connection-Pool/Nebenläufigkeitsobergrenze (welche die technische Argumentation des Labs ist, kein dokumentiertes Verhalten; hier wird keine konkrete Nebenläufigkeitszahl behauptet, weil Apple keine dokumentiert). ↩↩↩↩↩↩↩ -
SQLite, Write-Ahead Logging. Quelle für das WAL-Nebenläufigkeitsmodell: „WAL provides more concurrency as readers do not block writers and a writer does not block readers”, mit einem einzigen Schreiber zu jedem Zeitpunkt. ↩↩↩
-
Apple, Technical Q&A QA1809: Setting the SQLite journaling mode for a Core Data store. Quelle dafür, dass write-ahead logging seit iOS 7 und OS X Mavericks der Standard-Journaling-Modus für Core-Data-SQLite-Stores ist; SwiftData baut auf dem Core-Data-SQLite-Store auf. ↩↩↩
-
Apple,
ModelContext.fetchCount(_:). Signaturfunc fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel; liefert die Anzahl der zum Descriptor passenden Modelle, ohne sie zu instanziieren. ↩↩↩ -
Apple,
ModelContext.fetchIdentifiers(_:)undfetchIdentifiers(_:batchSize:). Liefert[PersistentIdentifier]für einen Fetch-Descriptor, ohne die Modelle zu materialisieren, mit einer Batch-Überladung. ↩↩↩ -
Apple,
PersistentIdentifier. Die zusammengesetzte Identität eines SwiftData-Modells; sie istSendable,HashableundCodable, was sie zum Typ macht, den man über Actor-Grenzen bewegt. Apple weist darauf hin, dass ein dekodierterPersistentIdentifierund einer, den der Standard-Store erzeugt hat, nicht immer als gleichwertig gelten — behandeln Sie ihn also als stabiles Context-übergreifendes Handle. ↩↩↩↩ -
Apple, Adopting SwiftData for a Core Data app. Quelle für das App-Group-Verhalten: Wenn eine App sich weiterentwickelt, um einen App-Group-Container zu nutzen, kopiert SwiftData unter der Standardkonfiguration den vorhandenen Store in den App-Group-Container; bei einer eigenen Store-URL verwalten Sie den Speicherort selbst. ↩
-
Apple, WWDC 2023 session 10189, Migrate to SwiftData, und
NSExpression. Quelle für Koexistenz („two completely separate persistent stacks, one Core Data stack and one SwiftData stack, talking to the same persistent store”), die Anforderung, dass beide dieselbe Store-URL verwenden und der Core-Data-StackNSPersistentHistoryTrackingKeyaktiviert oder der Store sich schreibgeschützt öffnet, sowie für Core Datas aufNSExpressionbasierende SQL-Aggregate, zu denen SwiftData kein Äquivalent bietet. ↩↩↩↩↩↩ -
Apple, WWDC 2026 session 274, What’s new in SwiftData. Quelle für
ResultsObserver, den auf Swift Observation basierenden Beobachtungstyp, der dieselben Primitive wie@Queryunterstützt, einschließlich der Key-Path-Sektionierung übersectionBy:, ausgeliefert in den Plattform-Releases von 2027. ↩ -
Apple, WWDC 2025 session 306, Optimize SwiftUI performance with Instruments, und die Instruments-Templates File Activity und Core Data. Quelle für das SwiftUI-Instruments-Template (das das SwiftUI-Instrument und die Hangs- und Hitches-Instrumente bündelt), das Reads-and-Writes-Instrument des File-Activity-Templates (nur auf dem Gerät) und das Data-Persistence-Instrument, das Faults, Fetches und Saves meldet. ↩↩
