← Tous les articles

La performance de SwiftData est un problème de stockage

Le group lab SwiftData de la WWDC 2026 était animé par les personnes qui possèdent les couches situées sous le framework, dont l’ingénieur qui maintient SQLite sur les plateformes d’Apple et le responsable de Core Data et de SwiftData. Le fil conducteur de leurs réponses était une correction utile à la façon dont la plupart des développeurs abordent la performance : une fois que SwiftData est dans votre application, la chose coûteuse est l’I/O, pas votre code Swift, et les gains viennent du fait de lire moins et de comprendre le moteur de stockage plutôt que d’ajouter de la concurrence. L’essentiel de ce qui suit s’appuie sur la documentation d’Apple et celle de SQLite ; lorsqu’une affirmation relève du raisonnement d’ingénierie du lab plutôt que d’un fait documenté, elle est signalée comme telle.

Watch: SwiftData Group Lab (WWDC26)

Le SwiftData Group Lab de la WWDC 2026.

En bref

  • Le store SQLite de SwiftData utilise le write-ahead logging (WAL) par défaut, ce qui signifie que plusieurs lecteurs s’exécutent en concurrence avec un unique rédacteur. Ce n’est pas un verrou lecteur/rédacteur, une distinction que les développeurs se trompent régulièrement, selon le lab.23
  • Lire sans matérialiser les objets : fetchCount(_:) renvoie un nombre de correspondances et fetchIdentifiers(_:) renvoie [PersistentIdentifier], l’un et l’autre sans hydrater les modèles. Associez-les à l’observation de l’historique pour décider si un rafraîchissement est même nécessaire.45
  • Les objets @Model ne sont pas Sendable et il ne faut pas les y forcer. Pour franchir une frontière d’acteur, transmettez le PersistentIdentifier (qui est Sendable) ainsi que les valeurs extraites, puis refaites la récupération sur le contexte de destination.6
  • SwiftData n’a aucun équivalent aux requêtes d’agrégation poussées en SQL de Core Data (sum, average, min, max). La porte de sortie est la coexistence : faites tourner une stack Core Data sur le même fichier de store et laissez-la calculer l’agrégat.8
  • Le message de performance du lab : profilez pour découvrir pourquoi SwiftUI a refait une récupération avant de supposer que la base de données est lente, car une sur-invalidation des vues ressemble à un problème d’I/O alors qu’elle n’en est pas un.110

WAL : lecteurs concurrents, un seul rédacteur, pas un verrou

La correction la plus utile du lab concerne la concurrence au niveau de la couche de stockage. SwiftData repose sur le store SQLite de Core Data, et ce store utilise par défaut le write-ahead logging depuis iOS 7.3 Sous WAL, comme le formule la documentation de SQLite, « WAL provides more concurrency as readers do not block writers and a writer does not block readers. Reading and writing can proceed concurrently. »2 Il y a toujours exactement un rédacteur à la fois, mais le modèle mental d’un mutex qui sérialise tous les accès à la base de données est faux : vos lectures n’ont pas à attendre derrière l’écriture.

Le cadrage du lab, paraphrasé depuis l’enregistrement, était que les gens traitent la règle de l’unique rédacteur comme un verrou lecteur/rédacteur et conçoivent leur architecture autour d’une contrainte qui n’existe pas.1 Le modèle exact est celui du WAL : concevoir pour de nombreuses lectures concurrentes et une écriture sérialisée, pas pour une exclusion globale.

Lire moins : compter et identifier sans hydrater

Si l’I/O est le coût, le geste à plus fort levier consiste à cesser de charger les objets dont vous n’avez pas besoin. SwiftData offre deux primitives pour cela, toutes deux vérifiées dans l’API :

fetchCount(_:) sur ModelContext prend un FetchDescriptor et renvoie le nombre de modèles correspondants sous forme d’Int sans en instancier aucun.4 Lorsque vous avez besoin d’un compte pour un badge ou un en-tête de section, c’est strictement moins coûteux que de récupérer les objets et d’appeler .count.

fetchIdentifiers(_:) renvoie [PersistentIdentifier] pour un descripteur, là encore sans matérialiser les modèles, et une surcharge fetchIdentifiers(_:batchSize:) traite le travail par lots.5 L’usage suggéré par le lab, paraphrasé, l’associe à l’observation de l’historique : lorsqu’un changement survient, récupérez les identifiants concernés et comparez-les à ce que votre vue affiche réellement avant de décider de recharger quoi que ce soit.1 Les API d’historique et d’observation elles-mêmes sont traitées dans Observation et historique de SwiftData sous iOS 27 ; fetchIdentifiers est la lecture légère qui les rend efficaces. Le type d’observation à privilégier en dehors de SwiftUI est ResultsObserver, l’observateur fondé sur Swift Observation introduit pour les versions 2027, qui prend en charge les mêmes primitives que @Query, dont le sectionnement par key-path via sectionBy:.9

La frontière Sendable est réelle, et le graphe de modèles ne la franchit pas

Les modèles SwiftData sont des types référence câblés dans un graphe à l’intérieur de leur contexte, et ils ne sont pas Sendable. Le lab a été catégorique sur le fait qu’on ne peut pas raisonnablement les y forcer, car le graphe n’est pas thread-safe et l’hydrater partiellement sur un autre acteur mène à des ennuis.1 Le pattern pris en charge utilise PersistentIdentifier, qui est Sendable, Hashable et Codable, comme identité que vous déplacez à travers les frontières.6 Extrayez les valeurs dont vous avez besoin dans une struct, attachez le PersistentIdentifier, transmettez-le à l’autre acteur, puis refaites la récupération du modèle sur le contexte de destination si vous avez besoin de l’objet vivant.

Une précision à conserver : Apple note qu’un PersistentIdentifier décodé et un identifiant créé par le store par défaut ne sont pas toujours considérés comme équivalents ; traitez donc l’identifiant comme un handle stable inter-contexte plutôt que de supposer qu’une copie décodée équivaut à un objet vivant.6

La même discipline « identité, pas graphe » se retrouve entre processus. Lorsque vous déplacez un store dans un app group pour le partager avec un widget ou une extension, la configuration par défaut copie pour vous le store existant dans le conteneur de l’app group ; avec une URL de store personnalisée, vous gérez vous-même l’emplacement.7 Dans les deux cas, les processus se coordonnent via le store et ses identifiants, et non en se transmettant des objets vivants.

Le déficit d’agrégation, et la porte de sortie Core Data

Une vraie limite nommée par le lab : SwiftData n’a aucun équivalent aux requêtes d’agrégation fondées sur NSExpression de Core Data, celles qui poussent sum, average, min et max dans SQLite afin que la base de données les calcule sans charger de lignes.8 Dans SwiftData, il faudrait récupérer les lignes et les réduire en mémoire, ce qui anéantit l’intérêt sur une grande table. Pour min ou max, vous pouvez récupérer avec un descripteur de tri et une limite de récupération à un ; pour de véritables agrégats, le lab a pointé vers la coexistence.

La coexistence, telle qu’Apple l’a cadrée à la WWDC 2023, ce sont « two completely separate persistent stacks, one Core Data stack and one SwiftData stack, talking to the same persistent store. »8 Les deux stacks pointent vers la même URL de store, et comme SwiftData active automatiquement le suivi de l’historique persistant, le côté Core Data doit lui aussi activer NSPersistentHistoryTrackingKey, sans quoi le store s’ouvre en lecture seule.8 Une fois cela en place, vous pouvez exécuter l’agrégat poussé en SQL via Core Data contre le fichier même que possède SwiftData. C’est davantage de machinerie que la plupart des applications n’en ont besoin, mais c’est le chemin documenté lorsque vous avez réellement besoin d’une agrégation côté base de données.

Profilez l’invalidation, pas seulement la base de données

Le conseil de performance le plus pratique du lab, paraphrasé, était que le coût d’I/O apparent d’une application SwiftData est souvent un problème d’invalidation SwiftUI déguisé : une vue qui s’invalide trop souvent refait des récupérations, et un profileur affiche ces récupérations comme du temps de base de données alors que la vraie faute est que la vue n’aurait pas dû se rafraîchir du tout.1 Le correctif est la même discipline d’isolation des vues qui aide tout problème de performance SwiftUI, traitée dans Performance et interopérabilité SwiftUI : découpez les grandes vues en plus petites, dotées de dépendances plus étroites, et transmettez vers le bas les modèles déjà récupérés afin que la requête ne se relance pas.

L’outillage soutient cette lecture. Instruments fournit un template SwiftUI qui regroupe l’instrument SwiftUI aux côtés des instruments Hangs et Hitches, un template File Activity dont l’instrument Reads and Writes montre le trafic disque réel (sur appareil uniquement, pas le simulateur), et le template Core Data avec son instrument Data Persistence qui rapporte les faults, les fetches et les saves.10 Faire tourner ensemble les vues SwiftUI et persistance vous indique si une récupération était une vraie lecture ou une lecture redondante déclenchée par une sur-invalidation.

Une mise en garde soulevée par le lab à propos du benchmarking, paraphrasée : il y a des caches à tous les niveaux, le cache de pages de SQLite, le cache de fichiers de l’OS et le contrôleur de stockage, de sorte qu’une exécution « rapide » peut être un cache hit plutôt qu’une amélioration réelle. Mesurez face à un jeu de données réalistement grand et utilisez l’instrument File Activity pour confirmer qu’une I/O réelle a bien eu lieu.1

À propos de l’ajout de concurrence

L’opinion la plus tranchée du lab, et la partie à traiter comme du raisonnement d’ingénierie plutôt que comme un fait documenté, était une mise en garde contre le réflexe de recourir à la concurrence comme correctif de performance. Les ingénieurs ont décrit le connection pooling de SwiftData comme délibérément borné et ont soutenu qu’au-delà d’un petit nombre d’opérations concurrentes vous atteignez le plafond du matériel de stockage, de sorte que davantage de contextes offre des rendements décroissants au prix de plus de mémoire et de plus d’I/O.1 Apple ne documente aucune limite de concurrence précise ; ne retenez donc de chiffre ferme de personne, y compris de ce billet. La conclusion défendable est directionnelle : sur un appareil à stockage flash, accumuler des rédacteurs concurrents n’est pas un moyen fiable d’aller plus vite, et le modèle WAL vous offre déjà gratuitement des lectures concurrentes.

Ce qu’il faut en retenir

Le lab recadre la performance de SwiftData autour du moteur de stockage. Les leviers vérifiés sont concrets : appuyez-vous sur les lectures concurrentes du WAL au lieu de craindre un verrou, utilisez fetchCount et fetchIdentifiers pour éviter d’hydrater des objets, déplacez PersistentIdentifier entre acteurs plutôt que le graphe de modèles, et recourez à la coexistence Core Data lorsque vous avez besoin d’un vrai agrégat. La discipline de profilage consiste à confirmer qu’un coût d’I/O est réel avant d’optimiser la base de données, car le coupable est souvent une vue qui s’est rafraîchie alors qu’elle n’aurait pas dû.

FAQ

SwiftData verrouille-t-il la base de données pendant les écritures ?

Pas au sens d’un verrou lecteur/rédacteur. Le store utilise le write-ahead logging de SQLite, qui permet à plusieurs lecteurs de s’exécuter en concurrence avec un unique rédacteur ; les lectures ne bloquent pas le rédacteur et le rédacteur ne bloque pas les lectures.23 Il y a un rédacteur à la fois, mais les lectures progressent à ses côtés.

Comment compter ou vérifier des enregistrements sans les charger ?

Utilisez ModelContext.fetchCount(_:) pour un nombre de correspondances et ModelContext.fetchIdentifiers(_:) pour des valeurs [PersistentIdentifier], ni l’un ni l’autre ne matérialisant les objets modèles.45 Combinez fetchIdentifiers avec l’observation de l’historique pour déterminer si un changement affecte réellement ce que votre vue montre avant de recharger.

Comment transmettre un objet SwiftData à un autre acteur ?

Vous ne transmettez pas l’objet. Les types @Model ne sont pas Sendable. Transmettez le PersistentIdentifier (qui est Sendable) ainsi que les valeurs extraites, puis refaites la récupération sur le contexte de destination.6 Évitez de faire franchir la frontière au graphe de modèles vivant.

SwiftData peut-il faire sum/average/min/max dans la base de données ?

Non. SwiftData n’a aucun équivalent aux agrégats NSExpression poussés en SQL de Core Data.8 Pour min/max, récupérez avec un tri et une limite de récupération à un ; pour de vrais agrégats, faites tourner une stack Core Data contre le même fichier de store (coexistence), ce qui requiert d’aligner l’URL de store et d’activer le suivi de l’historique persistant côté Core Data.8


La voie SwiftData de ce blog couvre la discipline de schéma et de migration dans discipline de schéma et le guide des migrations, et les API d’observation et d’historique d’iOS 27 dans observation et historique. Ce billet ajoute la couche de performance et de stockage. Le hub complet de la série est la série Apple Ecosystem.

References


  1. Apple, WWDC 2026 session 8017, SwiftData Group Lab. Paraphrased from a locally transcribed recording; Apple publishes no official captions for the labs, so the wording here is a paraphrase, not a quotation, and exact phrasing is unverified. Source for the reader/writer-lock misconception framing, the fetchIdentifiers-plus-history refresh-gating suggestion, the @Model non-Sendable transfer guidance, the view-invalidation-masquerading-as-I/O point, the “caches all the way down” benchmarking caution, and the connection-pool/concurrency-ceiling position (which is the lab’s engineering reasoning, not documented behavior; no specific concurrency number is asserted here because Apple does not document one). 

  2. SQLite, Write-Ahead Logging. Source for the WAL concurrency model: “WAL provides more concurrency as readers do not block writers and a writer does not block readers,” with a single writer at a time. 

  3. Apple, Technical Q&A QA1809: Setting the SQLite journaling mode for a Core Data store. Source for write-ahead logging being the default journaling mode for Core Data SQLite stores since iOS 7 and OS X Mavericks; SwiftData is built on the Core Data SQLite store. 

  4. Apple, ModelContext.fetchCount(_:). Signature func fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel; returns the number of models matching the descriptor without instantiating them. 

  5. Apple, ModelContext.fetchIdentifiers(_:) and fetchIdentifiers(_:batchSize:). Returns [PersistentIdentifier] for a fetch descriptor without materializing the models, with a batched overload. 

  6. Apple, PersistentIdentifier. The aggregate identity of a SwiftData model; it is Sendable, Hashable, and Codable, making it the type to move across actor boundaries. Apple notes a decoded PersistentIdentifier and one created by the default store are not always considered equivalent, so treat it as a stable cross-context handle. 

  7. Apple, Adopting SwiftData for a Core Data app. Source for the app-group behavior: when an app evolves to use an app group container, SwiftData copies the existing store into the app group container under the default configuration; with a custom store URL you manage the location yourself. 

  8. Apple, WWDC 2023 session 10189, Migrate to SwiftData, and NSExpression. Source for coexistence (“two completely separate persistent stacks, one Core Data stack and one SwiftData stack, talking to the same persistent store”), the requirement that both use the same store URL and that the Core Data stack enable NSPersistentHistoryTrackingKey or the store opens read-only, and for Core Data’s NSExpression-based SQL aggregates that SwiftData does not provide an equivalent to. 

  9. Apple, WWDC 2026 session 274, What’s new in SwiftData. Source for ResultsObserver, the Swift Observation-based observation type that supports the same primitives as @Query including key-path sectioning via sectionBy:, shipping in the 2027 platform releases. 

  10. Apple, WWDC 2025 session 306, Optimize SwiftUI performance with Instruments, and the Instruments File Activity and Core Data templates. Source for the SwiftUI Instruments template (bundling the SwiftUI instrument and the Hangs and Hitches instruments), the File Activity template’s Reads and Writes instrument (device only), and the Data Persistence instrument reporting faults, fetches, and saves. 

Articles connexes

L'impératif des scènes UIKit : ce qui ne se lancera plus sur iOS 27

Les apps compilées avec le SDK iOS 27 doivent adopter le cycle de vie UIKit basé sur les scènes, faute de quoi elles ne …

10 min de lecture

ImageCreator est déprécié : ce qui casse dans iOS 27

Apple abandonne la classe ImageCreator d'Image Playground dans iOS 27, avec des erreurs d'exécution dans TestFlight en b…

9 min de lecture

De 76 à 100 : obtenir un score Lighthouse parfait

Comment un site portfolio personnel est passé d'un score de performance mobile Lighthouse de 76 avec un CLS de 0,493 à u…

7 min de lecture