Performance Optimization
Optimizing Core Data performance requires understanding where bottlenecks occur and applying targeted solutions.
Profiling with Instruments
Time Profiler
- In Xcode: Product → Profile
- Select Time Profiler
- Record while using app
- Find heaviest stack traces
Look for:
- Excessive faulting
- Slow fetch requests
- Save operations taking too long
Allocations Instrument
- Product → Profile
- Select Allocations
- Monitor memory growth
- Identify retained objects
Look for:
- Unbounded memory growth
- Objects not being released
- Large allocations
SQL Debug Logging
Enable SQL logging:
-com.apple.CoreData.SQLDebug 1Output:
CoreData: sql: SELECT Z_PK, ZNAME FROM ZARTICLE WHERE ZVIEWS > ? LIMIT 20
CoreData: annotation: sql execution time: 0.0023sAnalyze:
- Query complexity
- Execution time
- Number of queries (N+1 problem)
Common Performance Issues
1. N+1 Query Problem
Problem:
// Fetches articles
let articles = try context.fetch(Article.fetchRequest())
// Each access fires a fault (N queries)
for article in articles {
print(article.category?.name) // Fault!
}Solution:
let fetchRequest = Article.fetchRequest()
fetchRequest.relationshipKeyPathsForPrefetching = ["category"]
let articles = try context.fetch(fetchRequest)
// No faults fired
for article in articles {
print(article.category?.name) // Already loaded
}2. Fetching Too Much Data
Problem:
// Fetches all properties of all objects
let articles = try context.fetch(Article.fetchRequest())
let count = articles.countSolution:
// Only counts, doesn't fetch objects
let count = try context.count(for: Article.fetchRequest())3. Not Using Batch Sizes
Problem:
// Loads 10,000 objects into memory
let fetchRequest = Article.fetchRequest()
let articles = try context.fetch(fetchRequest)Solution:
fetchRequest.fetchBatchSize = 20
// Only loads 20 at a time4. Fetching Unnecessary Properties
Problem:
// Fetches all properties
let fetchRequest = Article.fetchRequest()Solution:
fetchRequest.propertiesToFetch = ["name", "creationDate"]
// Only fetches needed properties5. Saving Too Frequently
Problem:
for item in items {
item.processed = true
try? context.save() // Very slow!
}Solution:
for item in items {
item.processed = true
}
try? context.save() // Save once6. Not Resetting Context
Problem:
// Context accumulates objects
for i in 0..<10000 {
let article = Article(context: context)
// Memory grows unbounded
}Solution:
for i in 0..<10000 {
let article = Article(context: context)
if i % 100 == 0 {
try? context.save()
context.reset() // Clear memory
}
}Memory Management
Context Reset
context.reset()When to use:
- After processing large batches
- When context accumulates many objects
- To free memory
Caution: Invalidates all fetched objects from this context.
Refresh Objects
context.refresh(article, mergeChanges: false)When to use:
- Discard in-memory changes
- Free memory for specific object
- Reload from database
Turn Objects into Faults
context.refreshAllObjects()When to use:
- Free memory across all objects
- After large operations
- When memory is constrained
Fetch Request Optimization
Checklist
let fetchRequest = Article.fetchRequest()
// ✅ Set batch size
fetchRequest.fetchBatchSize = 20
// ✅ Limit properties
fetchRequest.propertiesToFetch = ["name", "views"]
// ✅ Prefetch relationships
fetchRequest.relationshipKeyPathsForPrefetching = ["category"]
// ✅ Use predicate to filter
fetchRequest.predicate = NSPredicate(format: "views > %d", 100)
// ✅ Set fetch limit if applicable
fetchRequest.fetchLimit = 10
// ✅ Specify sort descriptors
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]Batch Operations
For large-scale operations, use batch requests:
// Instead of:
for article in articles {
article.isRead = true
}
try context.save()
// Use:
let batchUpdate = NSBatchUpdateRequest(entityName: "Article")
batchUpdate.propertiesToUpdate = ["isRead": true]
try context.execute(batchUpdate)Benefits:
- 10-20x faster
- Lower memory usage
- SQL-level operations
Data Generators for Testing
Create reproducible test datasets:
class DataGenerator {
func generate(count: Int, in context: NSManagedObjectContext) {
for i in 0..<count {
let article = Article(context: context)
article.name = "Article \(i)"
if i % 100 == 0 {
try? context.save()
context.reset()
}
}
try? context.save()
}
}
// Usage
let generator = DataGenerator()
generator.generate(count: 10000, in: backgroundContext)Profiling Checklist
- Enable SQL debug - See actual queries
- Profile with Time Profiler - Find slow operations
- Profile with Allocations - Find memory issues
- Test with realistic data - Small datasets hide problems
- Monitor on device - Simulator performance differs
- Test on older devices - Performance varies
Quick Wins
- Use
count(for:)instead of fetching - 100x faster - Set
fetchBatchSize- Reduces memory - Prefetch relationships - Eliminates N+1 queries
- Use
propertiesToFetch- Reduces data transfer - Reset context periodically - Frees memory
- Use batch operations - 10-20x faster for bulk changes
- Save conditionally - Check
hasPersistentChanges - Use background contexts - Keep UI responsive
Summary
- Profile first - Measure before optimizing
- Use Instruments - Time Profiler and Allocations
- Enable SQL debug - Understand query behavior
- Optimize fetch requests - Batch size, properties, prefetching
- Use batch operations - For large-scale changes
- Reset contexts - Free memory periodically
- Test with real data - Small datasets hide issues
- Monitor on devices - Real-world performance matters