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
        }
    }
}