← Todos os Posts

O desempenho do SwiftData é um problema de armazenamento

O group lab de SwiftData da WWDC 2026 foi conduzido pelas pessoas que cuidam das camadas que ficam por baixo do framework, incluindo o engenheiro que mantém o SQLite em todas as plataformas da Apple e o gerente do Core Data e do SwiftData. O fio condutor das respostas deles foi uma correção útil sobre como a maioria dos desenvolvedores busca desempenho: uma vez que o SwiftData está no seu app, o que custa caro é o I/O, não o seu código Swift, e os ganhos vêm de ler menos e de entender o storage engine, em vez de adicionar concorrência. A maior parte do que segue se baseia na documentação da Apple e do SQLite; onde uma afirmação é o raciocínio de engenharia do lab, e não um fato documentado, isso está sinalizado como tal.

Assista: SwiftData Group Lab (WWDC26)

O SwiftData Group Lab da WWDC 2026.

Resumo

  • O store SQLite do SwiftData usa write-ahead logging (WAL) por padrão, o que significa que múltiplos leitores rodam em paralelo com um único escritor. Não é um lock de leitor/escritor, uma distinção que, segundo o lab, os desenvolvedores costumam entender errado.23
  • Leia sem materializar objetos: fetchCount(_:) retorna uma contagem de correspondências e fetchIdentifiers(_:) retorna [PersistentIdentifier], ambos sem hidratar models. Combine-os com observação de histórico para decidir se um refresh é mesmo necessário.45
  • Objetos @Model não são Sendable e não devem ser forçados a sê-lo. Para cruzar a fronteira de um actor, passe o PersistentIdentifier (que é Sendable) junto com quaisquer valores extraídos e, então, busque novamente no contexto de destino.6
  • O SwiftData não tem equivalente às queries de agregação empurradas para o SQL do Core Data (sum, average, min, max). A saída de emergência é a coexistência: rodar um stack do Core Data contra o mesmo arquivo de store e deixar que ele compute a agregação.8
  • A mensagem de desempenho do lab: faça profiling para descobrir por que o SwiftUI refez a busca antes de presumir que o banco está lento, porque a invalidação excessiva de views se parece com um problema de I/O quando não é.110

WAL: leitores concorrentes, um escritor, não um lock

A correção mais útil do lab diz respeito à concorrência na camada de armazenamento. O SwiftData é construído sobre o store SQLite do Core Data, e esse store usa write-ahead logging por padrão desde o iOS 7.3 Sob WAL, como diz a documentação do 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 Ainda há exatamente um escritor por vez, mas o modelo mental de um mutex que serializa todo o acesso ao banco está errado: suas leituras não precisam esperar atrás da escrita.

O enquadramento do lab, parafraseado da gravação, foi que as pessoas tratam a regra de escritor único como um lock de leitor/escritor e projetam a arquitetura em torno de uma restrição que não existe.1 O modelo correto é o do WAL: projete para muitas leituras concorrentes e uma escrita serializada, não para exclusão global.

Leia menos: conte e identifique sem hidratar

Se o I/O é o custo, a jogada de maior alavancagem é parar de carregar objetos de que você não precisa. O SwiftData oferece dois primitivos para isso, ambos verificados na API:

fetchCount(_:) no ModelContext recebe um FetchDescriptor e retorna o número de models correspondentes como um Int sem instanciar nenhum deles.4 Quando você precisa de uma contagem para um badge ou um cabeçalho de seção, isso é estritamente mais barato do que buscar e chamar .count.

fetchIdentifiers(_:) retorna [PersistentIdentifier] para um descriptor, novamente sem materializar os models, e uma sobrecarga fetchIdentifiers(_:batchSize:) divide o trabalho em lotes.5 O uso sugerido pelo lab, parafraseado, combina isso com observação de histórico: quando uma mudança chega, busque os identificadores afetados e compare com o que sua view de fato exibe antes de decidir se vale recarregar qualquer coisa.1 As próprias APIs de histórico e observação são abordadas em observação e histórico do SwiftData no iOS 27; fetchIdentifiers é a leitura leve que as torna eficientes. O tipo de observação ao qual recorrer fora do SwiftUI é o ResultsObserver, o observer baseado em Swift Observation introduzido para os lançamentos de 2027, que suporta os mesmos primitivos do @Query, incluindo o seccionamento por key-path através de sectionBy:.9

A fronteira Sendable é real, e o grafo de models não a cruza

Os models do SwiftData são tipos de referência conectados a um grafo dentro do contexto deles, e não são Sendable. O lab foi direto ao dizer que você não pode, de forma sã, forçá-los a sê-lo, porque o grafo não é thread-safe e hidratá-lo parcialmente em outro actor leva a problemas.1 O padrão suportado usa o PersistentIdentifier, que é Sendable, Hashable e Codable, como a identidade que você move através das fronteiras.6 Extraia os valores de que você precisa para uma struct, anexe o PersistentIdentifier, entregue isso ao outro actor e busque novamente o model no contexto de destino se você precisar do objeto vivo.

Uma precisão que vale manter: a Apple observa que um PersistentIdentifier decodificado e outro criado pelo store padrão nem sempre são considerados equivalentes, então trate o identificador como um handle estável entre contextos, em vez de presumir que uma cópia decodificada é igual a uma viva.6

A mesma disciplina de identidade-não-grafo aparece entre processos. Quando você move um store para um app group a fim de compartilhá-lo com um widget ou uma extensão, a configuração padrão copia o store existente para o container do app group para você; com uma URL de store personalizada, você mesmo gerencia a localização.7 De qualquer forma, os processos se coordenam através do store e dos seus identificadores, não passando objetos vivos entre eles.

A lacuna de agregação, e a saída de emergência do Core Data

Uma limitação real que o lab apontou: o SwiftData não tem equivalente às queries de agregação baseadas em NSExpression do Core Data, aquelas que empurram sum, average, min e max para dentro do SQLite, de modo que o banco as compute sem carregar linhas.8 No SwiftData, você buscaria as linhas e reduziria em memória, o que anula o propósito numa tabela grande. Para min ou max, você pode buscar com um sort descriptor e um fetch limit de um; para agregações genuínas, o lab apontou a coexistência.

A coexistência, como a Apple a enquadrou na WWDC 2023, é “two completely separate persistent stacks, one Core Data stack and one SwiftData stack, talking to the same persistent store.”8 Ambos os stacks apontam para a mesma URL de store e, como o SwiftData habilita o rastreamento de histórico persistente automaticamente, o lado do Core Data também precisa habilitar NSPersistentHistoryTrackingKey, ou o store abre como somente leitura.8 Com isso no lugar, você pode rodar a agregação empurrada para o SQL através do Core Data contra o mesmo arquivo que o SwiftData controla. É mais maquinaria do que a maioria dos apps precisa, mas é o caminho documentado quando você genuinamente precisa de agregação do lado do banco.

Faça profiling da invalidação, não só do banco

A orientação de desempenho mais prática do lab, parafraseada, foi que o aparente custo de I/O de um app SwiftData muitas vezes é um problema de invalidação do SwiftUI disfarçado: uma view que invalida com frequência demais refaz a busca, e um profiler mostra essa nova busca como tempo de banco quando a culpa real é que a view não deveria ter dado refresh.1 A correção é a mesma disciplina de isolamento de views que ajuda em qualquer problema de desempenho do SwiftUI, abordada em desempenho e interoperabilidade do SwiftUI: quebre views grandes em outras menores com dependências mais estreitas e passe models já buscados para baixo, de modo que a query não rode de novo.

A ferramentaria apoia essa leitura. O Instruments traz um template SwiftUI que reúne o instrument SwiftUI ao lado dos instruments Hangs and Hitches, um template File Activity cujo instrument Reads and Writes mostra o tráfego real de disco (somente no dispositivo, não no simulador), e o template Core Data com seu instrument Data Persistence relatando faults, fetches e saves.10 Rodar as views de SwiftUI e de persistência juntas diz a você se uma nova busca foi uma leitura genuína ou uma redundante disparada por invalidação excessiva.

Uma ressalva que o lab levantou sobre benchmarking, parafraseada: há caches em todos os níveis, o page cache do SQLite, o cache de arquivos do SO e o controlador de armazenamento, então uma execução “rápida” pode ser um cache hit em vez de uma melhoria real. Meça contra um dataset realisticamente grande e use o instrument File Activity para confirmar que I/O de fato aconteceu.1

Sobre adicionar concorrência

A opinião mais forte do lab, e a parte a tratar como raciocínio de engenharia em vez de fato documentado, foi uma ressalva contra recorrer à concorrência como conserto de desempenho. Os engenheiros descreveram o pooling de conexões do SwiftData como deliberadamente limitado e argumentaram que, além de um pequeno número de operações concorrentes, você atinge o teto do hardware de armazenamento, de modo que mais contextos rendem retornos decrescentes ao custo de mais memória e mais I/O.1 A Apple não documenta um limite específico de concorrência, então não tire um número fixo de ninguém, incluindo deste post. A conclusão defensável é a direcional: num dispositivo com armazenamento flash, empilhar escritores concorrentes não é uma forma confiável de ir mais rápido, e o modelo WAL já lhe dá leituras concorrentes de graça.

O que tirar disso

O lab reenquadra o desempenho do SwiftData em torno do storage engine. As alavancas verificadas são concretas: apoie-se nas leituras concorrentes do WAL em vez de temer um lock, use fetchCount e fetchIdentifiers para evitar hidratar objetos, mova o PersistentIdentifier entre actors em vez do grafo de models e recorra à coexistência com o Core Data quando você precisar de uma agregação de verdade. A disciplina de profiling é confirmar que um custo de I/O é real antes de otimizar o banco, porque o culpado muitas vezes é uma view que deu refresh quando não deveria.

FAQ

O SwiftData trava o banco durante as escritas?

Não no sentido de lock de leitor/escritor. O store usa write-ahead logging do SQLite, que permite que múltiplos leitores rodem em paralelo com um único escritor; as leituras não bloqueiam o escritor e o escritor não bloqueia as leituras.23 Há um escritor por vez, mas as leituras prosseguem junto com ele.

Como conto ou verifico registros sem carregá-los?

Use ModelContext.fetchCount(_:) para uma contagem de correspondências e ModelContext.fetchIdentifiers(_:) para valores [PersistentIdentifier], nenhum dos quais materializa objetos model.45 Combine fetchIdentifiers com observação de histórico para decidir se uma mudança de fato afeta o que sua view mostra antes de recarregar.

Como passo um objeto SwiftData para outro actor?

Você não passa o objeto. Tipos @Model não são Sendable. Passe o PersistentIdentifier (que é Sendable) junto com quaisquer valores extraídos e, então, busque novamente no contexto de destino.6 Evite entregar o grafo de models vivo através da fronteira.

O SwiftData consegue fazer sum/average/min/max no banco?

Não. O SwiftData não tem equivalente às agregações empurradas para o SQL baseadas em NSExpression do Core Data.8 Para min/max, busque com um sort e um fetch limit de um; para agregações de verdade, rode um stack do Core Data contra o mesmo arquivo de store (coexistência), o que requer fazer a URL de store coincidir e habilitar o rastreamento de histórico persistente no lado do Core Data.8


A trilha de SwiftData neste blog cobre a disciplina de schema e migração em disciplina de schema e o guia de migrações, e as APIs de observação e histórico do iOS 27 em observação e histórico. Este post adiciona a camada de desempenho e armazenamento. O hub completo da série é a Apple Ecosystem Series.

Referências


  1. Apple, WWDC 2026 session 8017, SwiftData Group Lab. Parafraseado de uma gravação transcrita localmente; a Apple não publica legendas oficiais para os labs, então o texto aqui é uma paráfrase, não uma citação, e a redação exata não é verificada. Fonte para o enquadramento do equívoco do lock de leitor/escritor, a sugestão de gating de refresh com fetchIdentifiers mais histórico, a orientação de transferência de @Model não-Sendable, o ponto da invalidação de view se disfarçando de I/O, a ressalva de benchmarking dos “caches em todos os níveis” e a posição sobre teto de concorrência/pool de conexões (que é o raciocínio de engenharia do lab, não comportamento documentado; nenhum número específico de concorrência é afirmado aqui porque a Apple não documenta nenhum). 

  2. SQLite, Write-Ahead Logging. Fonte para o modelo de concorrência do WAL: “WAL provides more concurrency as readers do not block writers and a writer does not block readers,” com um único escritor por vez. 

  3. Apple, Technical Q&A QA1809: Setting the SQLite journaling mode for a Core Data store. Fonte para o write-ahead logging ser o modo de journaling padrão dos stores SQLite do Core Data desde o iOS 7 e o OS X Mavericks; o SwiftData é construído sobre o store SQLite do Core Data. 

  4. Apple, ModelContext.fetchCount(_:). Assinatura func fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel; retorna o número de models que correspondem ao descriptor sem instanciá-los. 

  5. Apple, ModelContext.fetchIdentifiers(_:) e fetchIdentifiers(_:batchSize:). Retorna [PersistentIdentifier] para um fetch descriptor sem materializar os models, com uma sobrecarga em lotes. 

  6. Apple, PersistentIdentifier. A identidade agregada de um model SwiftData; é Sendable, Hashable e Codable, o que o torna o tipo a mover através das fronteiras de actor. A Apple observa que um PersistentIdentifier decodificado e outro criado pelo store padrão nem sempre são considerados equivalentes, então trate-o como um handle estável entre contextos. 

  7. Apple, Adopting SwiftData for a Core Data app. Fonte para o comportamento do app group: quando um app evolui para usar um container de app group, o SwiftData copia o store existente para o container do app group sob a configuração padrão; com uma URL de store personalizada, você mesmo gerencia a localização. 

  8. Apple, WWDC 2023 session 10189, Migrate to SwiftData, e NSExpression. Fonte para a coexistência (“two completely separate persistent stacks, one Core Data stack and one SwiftData stack, talking to the same persistent store”), para a exigência de que ambos usem a mesma URL de store e de que o stack do Core Data habilite NSPersistentHistoryTrackingKey ou o store abra como somente leitura, e para as agregações SQL baseadas em NSExpression do Core Data às quais o SwiftData não fornece equivalente. 

  9. Apple, WWDC 2026 session 274, What’s new in SwiftData. Fonte para o ResultsObserver, o tipo de observação baseado em Swift Observation que suporta os mesmos primitivos do @Query, incluindo o seccionamento por key-path via sectionBy:, que chega nos lançamentos de plataforma de 2027. 

  10. Apple, WWDC 2025 session 306, Optimize SwiftUI performance with Instruments, e os templates File Activity e Core Data do Instruments. Fonte para o template SwiftUI do Instruments (reunindo o instrument SwiftUI e os instruments Hangs and Hitches), o instrument Reads and Writes do template File Activity (somente no dispositivo) e o instrument Data Persistence relatando faults, fetches e saves. 

Artigos relacionados

A obrigatoriedade de scenes do UIKit: o que não inicia no iOS 27

Apps compilados com o SDK do iOS 27 precisam adotar o ciclo de vida baseado em scenes do UIKit ou não iniciam. O cronogr…

9 min de leitura

ImageCreator foi descontinuada: o que quebra no iOS 27

A Apple está descontinuando a classe ImageCreator do Image Playground no iOS 27, com erros de runtime no TestFlight dura…

8 min de leitura

De 76 a 100: Alcançando uma Pontuação Perfeita no Lighthouse

Como um site pessoal de portfólio saiu de uma pontuação de 76 no Lighthouse mobile com 0,493 de CLS para um perfeito 100…

7 min de leitura