| name | performance |
| description | Otimiza performance de views SwiftUI — atualizacoes de estado, lazy loading, identidade em listas, otimizacao de imagens, e depuracao de re-renders inesperados. Use quando o usuario reportar lentidao, lag em scroll, travamentos, ou quiser auditar o desempenho de uma view. |
| argument-hint | audit | images | lists | state | debug |
Audite e otimize a performance de codigo SwiftUI seguindo as melhores praticas.
Argumentos
Tipo de otimizacao: $ARGUMENTS
Se $ARGUMENTS estiver vazio, pergunte:
- "Qual aspecto de performance precisa otimizar?"
audit — revisao geral de uma view ou componente
state — atualizacoes de estado desnecessarias, dependencias largas
lists — scroll lento, lazy loading, identidade em ForEach
images — memoria alta, imagens grandes, downsampling
debug — identificar qual estado esta causando re-renders
Opcao 1: audit — Revisao geral
Execute o seguinte checklist sobre a view em questao e aponte cada violacao encontrada:
Anti-patterns para detectar
Objeto criado dentro do body
var body: some View {
let formatter = DateFormatter()
return Text(formatter.string(from: date))
}
private static let dateFormatter: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .long
return f
}()
var body: some View {
Text(Self.dateFormatter.string(from: date))
}
Computacao pesada dentro do body
var body: some View {
List(items.sorted { $0.name < $1.name }) { ... }
}
@State private var sortedItems: [Item] = []
var body: some View {
List(sortedItems) { ... }
.onChange(of: items) { _, newItems in
sortedItems = newItems.sorted { $0.name < $1.name }
}
}
Mova sorting, filtering e formatting para o ViewModel ou computed properties. O body deve ser pura representacao estrutural do estado.
Estado derivado armazenado desnecessariamente
@State private var items: [Item] = []
@State private var itemCount: Int = 0
@State private var items: [Item] = []
var itemCount: Int { items.count }
Update de estado sem verificacao de mudanca
.onReceive(publisher) { value in
self.currentValue = value
}
.onReceive(publisher) { value in
if self.currentValue != value {
self.currentValue = value
}
}
Animacao em hot path
.onPreferenceChange(ScrollOffsetKey.self) { offset in
withAnimation { self.offset = offset.y }
}
.onPreferenceChange(ScrollOffsetKey.self) { offset in
let shouldShow = offset.y < -50
if shouldShow != showTitle {
withAnimation(.easeOut(duration: 0.2)) { showTitle = shouldShow }
}
}
Escopo de animacao muito amplo
VStack {
HeaderView()
ExpandableContent(isExpanded: isExpanded)
FooterView()
}
.animation(.spring, value: isExpanded)
VStack {
HeaderView()
ExpandableContent(isExpanded: isExpanded)
.animation(.spring, value: isExpanded)
FooterView()
}
Closures off-main-thread acessando estado @MainActor
.visualEffect { content, geometry in
content.blur(radius: self.pulse ? 5 : 0)
}
.visualEffect { [pulse] content, geometry in
content.blur(radius: pulse ? 5 : 0)
}
Closures que podem rodar fora da main thread: Shape.path(in:), visualEffect, Layout protocol, onGeometryChange transform.
Opcao 2: state — Dependencias de estado
Passe apenas o que a view precisa
struct ItemRow: View {
@Environment(AppModel.self) private var model
let item: Item
var body: some View {
Text(item.name).foregroundStyle(model.theme.primaryColor)
}
}
struct ItemRow: View {
let item: Item
let themeColor: Color
var body: some View {
Text(item.name).foregroundStyle(themeColor)
}
}
ViewModel por item em listas (iOS 17+ com @Observable)
@Observable class ModelData {
var favorites: [Landmark] = []
}
struct LandmarkRow: View {
@Environment(ModelData.self) private var model
let landmark: Landmark
var body: some View {
if model.favorites.contains(landmark) { ... }
}
}
@Observable class LandmarkViewModel {
var isFavorite: Bool = false
}
struct LandmarkRow: View {
let landmark: Landmark
let viewModel: LandmarkViewModel
var body: some View {
if viewModel.isFavorite { ... }
}
}
Requer @Observable (iOS 17+). Gateie com #available(iOS 17, *) ou mantenha o padrao ObservableObject para iOS 15.
Views Equatable para bodies caros
struct ExpensiveView: View, Equatable {
let data: SomeData
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.data.id == rhs.data.id
}
var body: some View { }
}
ExpensiveView(data: data).equatable()
Cuidado: se adicionar novas dependencias a view, atualize o ==.
Views POD (Plain Old Data) para diffing rapido
Views com apenas let e sem property wrappers usam memcmp — o diffing mais rapido possivel.
struct FastRow: View {
let title: String
let count: Int
var body: some View { Text("\(title): \(count)") }
}
struct ExpensiveView: View {
let value: Int
var body: some View { ExpensiveViewInternal(value: value) }
}
private struct ExpensiveViewInternal: View {
let value: Int
@State private var item: Item?
var body: some View { }
}
Opcao 3: lists — Listas e scroll
Lazy containers obrigatorios para listas longas
ScrollView {
VStack {
ForEach(items) { item in ExpensiveRow(item: item) }
}
}
ScrollView {
LazyVStack {
ForEach(items) { item in ExpensiveRow(item: item) }
}
}
Identidade estavel em ForEach
ForEach(items.indices, id: \.self) { index in
ItemRow(item: items[index])
}
ForEach(items) { item in
ItemRow(item: item)
}
ForEach(items, id: \.stableID) { item in
ItemRow(item: item)
}
Identidade instavel causa transicoes erradas, scroll jumps e re-renders completos da lista.
Task por item para cancelamento correto
List(items) { item in
ItemRow(item: item)
.task(id: item.id) {
await item.loadDetails()
}
}
Opcao 4: images — Imagens e memoria
AsyncImage com tratamento de todos os estados
AsyncImage(url: imageURL) { phase in
switch phase {
case .empty:
ProgressView()
.frame(width: 200, height: 200)
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.transition(.opacity)
case .failure:
Image(systemName: "photo")
.foregroundStyle(.secondary)
.frame(width: 200, height: 200)
@unknown default:
EmptyView()
}
}
.animation(.easeInOut, value: imageURL)
Downsampling para UIImage(data:) (otimizacao opcional)
Sugira quando encontrar UIImage(data:) em listas, grids ou galerias.
Image(uiImage: UIImage(data: imageData)!)
struct DownsampledImage: View {
let imageData: Data
let targetSize: CGSize
@State private var image: UIImage?
var body: some View {
Group {
if let image {
Image(uiImage: image).resizable().aspectRatio(contentMode: .fill)
} else {
ProgressView()
}
}
.task {
image = await downsample(imageData, to: targetSize)
}
}
private func downsample(_ data: Data, to size: CGSize) async -> UIImage? {
await Task.detached {
let sourceOptions: [CFString: Any] = [kCGImageSourceShouldCache: false]
guard let source = CGImageSourceCreateWithData(data as CFData, sourceOptions as CFDictionary) else { return nil }
let scale = await UIScreen.main.scale
let maxPixel = max(size.width, size.height) * scale
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceThumbnailMaxPixelSize: maxPixel,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceShouldCacheImmediately: true
]
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else { return nil }
return UIImage(cgImage: cgImage)
}.value
}
}
kCGImageSourceShouldCache: false evita cache da imagem em tamanho original.
kCGImageSourceShouldCacheImmediately: true forca decodificacao na criacao (nao no primeiro render).
UIImage(named:) vs UIImage(contentsOfFile:)
let image = UIImage(named: "Wallpapers/photo_001.jpg")
if let path = Bundle.main.path(forResource: "Wallpapers/photo_001", ofType: "jpg") {
let image = UIImage(contentsOfFile: path)
}
NSCache com limite definido
private let imageCache: NSCache<NSString, UIImage> = {
let cache = NSCache<NSString, UIImage>()
cache.countLimit = 50
return cache
}()
Opcao 5: debug — Identificar re-renders
Self._logChanges() / Self._printChanges()
struct MinhaView: View {
var body: some View {
#if DEBUG
let _ = Self._logChanges()
#endif
}
}
_printChanges() — imprime no stdout qual propriedade mudou
_logChanges() — loga via os_log (visivel no Console.app e Instruments)
- Ambos imprimem
@self quando o valor da view mudou e @identity quando a identidade persistente foi reciclada
Estrategia de diagnostico
- Adicione
Self._logChanges() na view suspeita
- Identifique qual propriedade dispara o re-render inesperado
- Extraia a parte que muda em uma subview separada — SwiftUI pode pular o body de subviews quando os inputs nao mudaram
- Se o re-render for de um estado derivado, converta para computed property
- Se for de dependencia ampla, estreite passando apenas os valores necessarios
- Profile com Instruments > SwiftUI template para bottlenecks persistentes
Problemas comuns identificados por _logChanges
| Log impresso | Causa provavel | Solucao |
|---|
@self changed | Valor da view recriado pelo pai | Extrair subview ou usar .equatable() |
@identity changed | Identidade reciclada | Verificar estabilidade do id no ForEach |
somePublished changed | @Published atualizado frequentemente | Verificar igualdade antes de atribuir |
environment changed | Valor no environment mudando | Evitar valores de alta frequencia no environment |
Checklist de Revisao
Hard rules
Performance
Diagnostico