← Todos los articulos

El rendimiento de SwiftData es un problema de almacenamiento

El lab grupal de SwiftData en la WWDC 2026 estuvo a cargo de las personas que mantienen las capas que hay debajo del framework, entre ellas el ingeniero que mantiene SQLite en las plataformas de Apple y el manager de Core Data y SwiftData. El hilo conductor de sus respuestas fue una corrección útil sobre cómo la mayoría de los desarrolladores buscan rendimiento: una vez que SwiftData está en tu app, lo costoso es el I/O, no tu código Swift, y las ganancias vienen de leer menos y de entender el motor de almacenamiento, no de agregar concurrencia. Casi todo lo que sigue se apoya en la documentación de Apple y de SQLite; donde una afirmación es el razonamiento de ingeniería del lab y no un hecho documentado, se señala como tal.

Ver: SwiftData Group Lab (WWDC26)

El SwiftData Group Lab de la WWDC 2026.

Resumen

  • El store de SQLite de SwiftData usa write-ahead logging (WAL) de forma predeterminada, lo que significa que múltiples lectores se ejecutan de manera concurrente con un único escritor. No es un bloqueo lector/escritor, una distinción que, según el lab, los desarrolladores suelen entender mal.23
  • Lee sin materializar objetos: fetchCount(_:) devuelve un conteo de coincidencias y fetchIdentifiers(_:) devuelve [PersistentIdentifier], ambos sin hidratar modelos. Combínalos con la observación del historial para decidir si siquiera hace falta refrescar.45
  • Los objetos @Model no son Sendable y no deberías forzarlos a serlo. Para cruzar una frontera de actor, pasa el PersistentIdentifier (que sí es Sendable) junto con los valores que hayas extraído, y luego vuelve a hacer fetch en el contexto de destino.6
  • SwiftData no tiene un equivalente a las consultas de agregación que Core Data empuja a SQL (sum, average, min, max). La salida de emergencia es la coexistencia: ejecuta un stack de Core Data contra el mismo archivo de store y deja que él calcule el agregado.8
  • El mensaje del lab sobre rendimiento: perfila para averiguar por qué SwiftUI volvió a hacer fetch antes de suponer que la base de datos es lenta, porque la sobre-invalidación de vistas parece un problema de I/O cuando no lo es.110

WAL: lectores concurrentes, un escritor, no un bloqueo

La corrección más útil del lab tiene que ver con la concurrencia en la capa de almacenamiento. SwiftData está construido sobre el store de SQLite de Core Data, y ese store usa write-ahead logging de forma predeterminada desde iOS 7.3 Bajo WAL, como lo expresa la documentación de SQLite, “WAL ofrece más concurrencia, ya que los lectores no bloquean a los escritores y un escritor no bloquea a los lectores. La lectura y la escritura pueden avanzar de forma concurrente.”2 Sigue habiendo exactamente un escritor a la vez, pero el modelo mental de un mutex que serializa todo el acceso a la base de datos es erróneo: tus lecturas no tienen que esperar detrás de la escritura.

El enfoque del lab, parafraseado de la grabación, fue que la gente trata la regla de un solo escritor como un bloqueo lector/escritor y diseña su arquitectura en torno a una restricción que no existe.1 El modelo correcto es el de WAL: diseña para muchas lecturas concurrentes y una escritura serializada, no para una exclusión global.

Lee menos: cuenta e identifica sin hidratar

Si el I/O es el costo, la jugada de mayor apalancamiento es dejar de cargar objetos que no necesitas. SwiftData ofrece dos primitivas para esto, ambas verificadas en la API:

fetchCount(_:) en ModelContext recibe un FetchDescriptor y devuelve el número de modelos coincidentes como un Int sin instanciar ninguno de ellos.4 Cuando necesitas un conteo para un badge o un encabezado de sección, esto es estrictamente más barato que hacer fetch y llamar a .count.

fetchIdentifiers(_:) devuelve [PersistentIdentifier] para un descriptor, de nuevo sin materializar los modelos, y una sobrecarga fetchIdentifiers(_:batchSize:) divide el trabajo en lotes.5 El uso sugerido por el lab, parafraseado, combina esto con la observación del historial: cuando llega un cambio, haz fetch de los identificadores afectados y compáralos con lo que tu vista realmente muestra antes de decidir si recargar algo.1 Las APIs de historial y observación en sí se cubren en La observación y el historial de SwiftData en iOS 27; fetchIdentifiers es la lectura ligera que las hace eficientes. El tipo de observación al que recurrir fuera de SwiftUI es ResultsObserver, el observador basado en Swift Observation introducido para los lanzamientos de 2027, que admite las mismas primitivas que @Query, incluido el seccionamiento por key path mediante sectionBy:.9

La frontera Sendable es real, y el grafo de modelos no la cruza

Los modelos de SwiftData son tipos por referencia cableados a un grafo dentro de su contexto, y no son Sendable. El lab fue tajante: no puedes forzarlos a serlo de forma sensata, porque el grafo no es seguro para hilos y hidratarlo parcialmente en otro actor genera problemas.1 El patrón admitido usa PersistentIdentifier, que es Sendable, Hashable y Codable, como la identidad que mueves entre fronteras.6 Extrae los valores que necesites en una estructura, adjunta el PersistentIdentifier, entrégalo al otro actor y vuelve a hacer fetch del modelo en el contexto de destino si necesitas el objeto vivo.

Una precisión que conviene tener presente: Apple advierte que un PersistentIdentifier decodificado y uno creado por el store predeterminado no siempre se consideran equivalentes, así que trata el identificador como un manejador estable entre contextos en lugar de suponer que una copia decodificada equivale a una viva.6

La misma disciplina de identidad-no-grafo aparece entre procesos. Cuando mueves un store a un app group para compartirlo con un widget o una extensión, la configuración predeterminada copia el store existente al contenedor del app group por ti; con una URL de store personalizada, gestionas la ubicación tú mismo.7 En cualquier caso, los procesos se coordinan a través del store y sus identificadores, no pasándose objetos vivos entre ellos.

La brecha de agregados, y la salida de emergencia de Core Data

Una limitación real que el lab señaló: SwiftData no tiene un equivalente a las consultas de agregación de Core Data basadas en NSExpression, esas que empujan sum, average, min y max hacia SQLite para que la base de datos las calcule sin cargar filas.8 En SwiftData tendrías que hacer fetch de las filas y reducirlas en memoria, lo que anula el propósito en una tabla grande. Para min o max puedes hacer fetch con un sort descriptor y un límite de fetch de uno; para agregados genuinos, el lab apuntó a la coexistencia.

La coexistencia, tal como Apple la planteó en la WWDC 2023, son “dos stacks de persistencia completamente separados, un stack de Core Data y un stack de SwiftData, hablando con el mismo persistent store.”8 Ambos stacks apuntan a la misma URL de store y, como SwiftData habilita el seguimiento del historial persistente automáticamente, el lado de Core Data también debe habilitar NSPersistentHistoryTrackingKey o el store se abre en modo de solo lectura.8 Con eso en su lugar, puedes ejecutar el agregado empujado a SQL a través de Core Data contra el mismísimo archivo que SwiftData posee. Es más maquinaria de la que la mayoría de las apps necesita, pero es el camino documentado cuando realmente requieres agregación del lado de la base de datos.

Perfila la invalidación, no solo la base de datos

La guía de rendimiento más práctica del lab, parafraseada, fue que el aparente costo de I/O de una app de SwiftData a menudo es un problema de invalidación de SwiftUI disfrazado: una vista que se invalida demasiado seguido vuelve a hacer fetch, y un profiler muestra ese re-fetch como tiempo de base de datos cuando la verdadera falla es que la vista no debió refrescarse en absoluto.1 La solución es la misma disciplina de aislamiento de vistas que ayuda con cualquier problema de rendimiento de SwiftUI, cubierta en Rendimiento e interoperabilidad de SwiftUI: divide las vistas grandes en otras más pequeñas con dependencias más acotadas, y pasa hacia abajo los modelos ya obtenidos para que la query no se vuelva a ejecutar.

Las herramientas respaldan esta lectura. Instruments incluye una plantilla de SwiftUI que agrupa el instrumento de SwiftUI junto con los instrumentos de Hangs y Hitches, una plantilla de File Activity cuyo instrumento de Reads y Writes muestra el tráfico de disco real (solo en dispositivo, no en el simulador), y la plantilla de Core Data con su instrumento de Data Persistence que reporta faults, fetches y saves.10 Ejecutar juntas las vistas de SwiftUI y de persistencia te dice si un re-fetch fue una lectura genuina o una redundante disparada por sobre-invalidación.

Una advertencia que el lab planteó sobre el benchmarking, parafraseada: hay cachés por todas partes, el caché de páginas de SQLite, el caché de archivos del sistema operativo y el controlador de almacenamiento, así que una ejecución “rápida” puede ser un acierto de caché y no una mejora real. Mide contra un conjunto de datos realistamente grande y usa el instrumento de File Activity para confirmar que de verdad ocurrió I/O.1

Sobre agregar concurrencia

La opinión más firme del lab, y la parte que conviene tratar como razonamiento de ingeniería y no como hecho documentado, fue una advertencia contra recurrir a la concurrencia como arreglo de rendimiento. Los ingenieros describieron el connection pooling de SwiftData como deliberadamente acotado y argumentaron que, más allá de un pequeño número de operaciones concurrentes, chocas con el techo del hardware de almacenamiento, así que más contextos rinden cada vez menos a cambio de más memoria y más I/O.1 Apple no documenta un límite de concurrencia específico, así que no tomes una cifra exacta de nadie, este post incluido. La conclusión defendible es la direccional: en un dispositivo con almacenamiento flash, apilar escritores concurrentes no es una forma confiable de ir más rápido, y el modelo WAL ya te da lecturas concurrentes gratis.

Qué llevarse de esto

El lab replantea el rendimiento de SwiftData en torno al motor de almacenamiento. Las palancas verificadas son concretas: apóyate en las lecturas concurrentes de WAL en lugar de temer un bloqueo, usa fetchCount y fetchIdentifiers para evitar hidratar objetos, mueve el PersistentIdentifier entre actores en vez del grafo de modelos, y recurre a la coexistencia con Core Data cuando necesites un agregado de verdad. La disciplina de perfilado consiste en confirmar que un costo de I/O es real antes de optimizar la base de datos, porque el culpable suele ser una vista que se refrescó cuando no debía.

Preguntas frecuentes

¿SwiftData bloquea la base de datos durante las escrituras?

No en el sentido de un bloqueo lector/escritor. El store usa el write-ahead logging de SQLite, que permite que múltiples lectores se ejecuten de manera concurrente con un único escritor; las lecturas no bloquean al escritor y el escritor no bloquea las lecturas.23 Hay un escritor a la vez, pero las lecturas avanzan en paralelo.

¿Cómo cuento o reviso registros sin cargarlos?

Usa ModelContext.fetchCount(_:) para un conteo de coincidencias y ModelContext.fetchIdentifiers(_:) para valores [PersistentIdentifier], ninguno de los cuales materializa objetos de modelo.45 Combina fetchIdentifiers con la observación del historial para decidir si un cambio realmente afecta lo que tu vista muestra antes de recargar.

¿Cómo paso un objeto de SwiftData a otro actor?

No pasas el objeto. Los tipos @Model no son Sendable. Pasa el PersistentIdentifier (que sí es Sendable) junto con los valores que hayas extraído, y luego vuelve a hacer fetch en el contexto de destino.6 Evita entregar el grafo de modelos vivo a través de la frontera.

¿Puede SwiftData hacer sum/average/min/max en la base de datos?

No. SwiftData no tiene un equivalente a los agregados de Core Data empujados a SQL mediante NSExpression.8 Para min/max, haz fetch con un sort y un límite de fetch de uno; para agregados verdaderos, ejecuta un stack de Core Data contra el mismo archivo de store (coexistencia), lo que requiere que coincida la URL del store y que se habilite el seguimiento del historial persistente del lado de Core Data.8


El carril de SwiftData en este blog cubre la disciplina de esquema y migración en disciplina de esquema y la guía de migraciones, y las APIs de observación e historial de iOS 27 en observación e historial. Este post añade la capa de rendimiento y almacenamiento. El hub completo de la serie es la Serie del ecosistema Apple.

Referencias


  1. Apple, sesión 8017 de la WWDC 2026, SwiftData Group Lab. Parafraseado de una grabación transcrita localmente; Apple no publica subtítulos oficiales para los labs, así que la redacción aquí es una paráfrasis, no una cita, y la formulación exacta no está verificada. Fuente del enfoque sobre el malentendido del bloqueo lector/escritor, la sugerencia de condicionar el refresco con fetchIdentifiers más historial, la guía de transferencia de @Model como no-Sendable, el punto de la invalidación de vistas haciéndose pasar por I/O, la advertencia de benchmarking sobre “cachés por todas partes” y la postura sobre el connection pool y el techo de concurrencia (que es el razonamiento de ingeniería del lab, no un comportamiento documentado; aquí no se afirma ninguna cifra específica de concurrencia porque Apple no documenta ninguna). 

  2. SQLite, Write-Ahead Logging. Fuente del modelo de concurrencia de WAL: “WAL ofrece más concurrencia, ya que los lectores no bloquean a los escritores y un escritor no bloquea a los lectores”, con un único escritor a la vez. 

  3. Apple, Technical Q&A QA1809: Setting the SQLite journaling mode for a Core Data store. Fuente de que el write-ahead logging es el modo de journaling predeterminado para los stores de SQLite de Core Data desde iOS 7 y OS X Mavericks; SwiftData está construido sobre el store de SQLite de Core Data. 

  4. Apple, ModelContext.fetchCount(_:). Firma func fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel; devuelve el número de modelos que coinciden con el descriptor sin instanciarlos. 

  5. Apple, ModelContext.fetchIdentifiers(_:) y fetchIdentifiers(_:batchSize:). Devuelve [PersistentIdentifier] para un fetch descriptor sin materializar los modelos, con una sobrecarga por lotes. 

  6. Apple, PersistentIdentifier. La identidad agregada de un modelo de SwiftData; es Sendable, Hashable y Codable, lo que lo convierte en el tipo a mover a través de las fronteras de actor. Apple advierte que un PersistentIdentifier decodificado y uno creado por el store predeterminado no siempre se consideran equivalentes, así que trátalo como un manejador estable entre contextos. 

  7. Apple, Adopting SwiftData for a Core Data app. Fuente del comportamiento del app group: cuando una app evoluciona para usar un contenedor de app group, SwiftData copia el store existente al contenedor del app group bajo la configuración predeterminada; con una URL de store personalizada, gestionas la ubicación tú mismo. 

  8. Apple, sesión 10189 de la WWDC 2023, Migrate to SwiftData, y NSExpression. Fuente de la coexistencia (“dos stacks de persistencia completamente separados, un stack de Core Data y un stack de SwiftData, hablando con el mismo persistent store”), del requisito de que ambos usen la misma URL de store y de que el stack de Core Data habilite NSPersistentHistoryTrackingKey o el store se abre en modo de solo lectura, y de los agregados SQL de Core Data basados en NSExpression a los que SwiftData no ofrece un equivalente. 

  9. Apple, sesión 274 de la WWDC 2026, What’s new in SwiftData. Fuente de ResultsObserver, el tipo de observación basado en Swift Observation que admite las mismas primitivas que @Query, incluido el seccionamiento por key path mediante sectionBy:, que llega en los lanzamientos de plataforma de 2027. 

  10. Apple, sesión 306 de la WWDC 2025, Optimize SwiftUI performance with Instruments, y las plantillas de File Activity y Core Data de Instruments. Fuente de la plantilla de SwiftUI de Instruments (que agrupa el instrumento de SwiftUI y los instrumentos de Hangs y Hitches), del instrumento de Reads y Writes de la plantilla de File Activity (solo en dispositivo) y del instrumento de Data Persistence que reporta faults, fetches y saves. 

Artículos relacionados

El mandato de escenas de UIKit: qué deja de iniciarse en iOS 27

Las apps creadas con el SDK de iOS 27 deben adoptar el ciclo de vida de UIKit basado en escenas o no se inician. El cron…

9 min de lectura

ImageCreator queda obsoleta: qué se rompe en iOS 27

Apple descontinúa la clase ImageCreator de Image Playground en iOS 27, con errores de ejecución en TestFlight durante la…

9 min de lectura

De 76 a 100: Cómo lograr una puntuación perfecta en Lighthouse

Cómo un sitio de portafolio personal pasó de una puntuación de rendimiento móvil en Lighthouse de 76 con un CLS de 0,493…

7 min de lectura