Section 10/181 menit
10. Performance Optimization
10. Performance Optimization
Indexing untuk Query Cepat
swift
// Tambahkan index pada property yang sering dipakai sebagai filter
@Model
final class LogEntry {
// Index pada timestamp — query "fetch last 100 entries" sangat cepat
@Attribute(.index)
var timestamp: Date
// Index pada level — query "fetch all error logs" sangat cepat
@Attribute(.index)
var level: String // "debug", "info", "warning", "error"
var message: String
var metadata: [String: String]
init(level: String, message: String) {
self.timestamp = Date.now
self.level = level
self.message = message
self.metadata = [:]
}
}
// Dengan index: query ini O(log n) bukan O(n)
let errorLogs = FetchDescriptor<LogEntry>(
predicate: #Predicate { $0.level == "error" },
sortBy: [SortDescriptor(\LogEntry.timestamp, order: .reverse)]
)
Relationship Prefetching untuk N+1 Query Prevention
swift
// ❌ TANPA prefetch: N+1 queries
func displayPosts(context: ModelContext) throws {
let posts = try context.fetch(FetchDescriptor<Post>())
for post in posts {
// Setiap akses post.author → query terpisah = N queries!
print("\(post.title) by \(post.author?.name ?? "Unknown")")
}
}
// ✓ DENGAN prefetch: 1 query JOIN
func displayPostsOptimized(context: ModelContext) throws {
var descriptor = FetchDescriptor<Post>()
descriptor.relationshipKeyPathsForPrefetching = [\.author, \.tags]
let posts = try context.fetch(descriptor)
for post in posts {
// author dan tags sudah di-load semuanya → 0 extra queries
print("\(post.title) by \(post.author?.name ?? "Unknown")")
print("Tags: \(post.tags.map(\.name).joined(separator: ", "))")
}
}
FetchDescriptor Optimization
swift
// Fetch dengan limit dan offset (pagination)
var descriptor = FetchDescriptor<Article>(
sortBy: [SortDescriptor(\Article.publishedAt, order: .reverse)]
)
descriptor.fetchLimit = 20 // Hanya ambil 20 item
descriptor.fetchOffset = 40 // Mulai dari item ke-41 (halaman 3)
descriptor.includePendingChanges = false // Skip unsaved changes (lebih cepat)
// Fetch hanya property tertentu (tidak perlu semua kolom)
// Sayangnya SwiftData belum support projection seperti Core Data NSFetchRequest.propertiesToFetch
// Workaround: fetch ID saja, lalu lazy load saat dibutuhkan
var idDescriptor = FetchDescriptor<Article>()
idDescriptor.fetchLimit = 100
let articles = try context.fetch(idDescriptor)
let ids = articles.map(\.persistentModelID)
// Kemudian fetch detail satu per satu saat dibutuhkan (on-demand)
func getArticle(id: PersistentIdentifier, context: ModelContext) -> Article? {
context.model(for: id) as? Article
}
Menggunakan fetchCount untuk Badge/Count Display
swift
// Tampilkan count di UI tanpa load semua data
struct TabView: View {
@Environment(\.modelContext) private var context
@State private var pendingCount: Int = 0
var body: some View {
TabView {
TaskListView()
.tabItem {
Label("Tugas", systemImage: "checklist")
}
.badge(pendingCount)
}
.task {
let descriptor = FetchDescriptor<Task>(
predicate: #Predicate { !$0.isCompleted }
)
pendingCount = (try? context.fetchCount(descriptor)) ?? 0
}
}
}