performance-optimization
Performance Optimization — Expert Decisions
Expert decision frameworks for performance choices. Claude knows lazy loading and async basics — this skill provides judgment calls for when to optimize and which tool to use.
Decision Trees
Should You Optimize?
When should you invest in optimization?
├─ User-facing latency issue (visible stutter/delay)
│ └─ YES — Profile and fix
│ Measure first, optimize second
│
├─ Premature concern ("this might be slow")
│ └─ NO — Wait for evidence
│ Write clean code, profile later
│
├─ Battery drain complaints
│ └─ YES — Use Energy Diagnostics
│ Focus on background work, location, network
│
├─ Memory warnings / crashes
│ └─ YES — Use Allocations + Leaks
│ Find retain cycles, unbounded caches
│
└─ App store reviews mention slowness
└─ YES — Profile real scenarios
User perception matters
The trap: Optimizing based on assumptions. Always profile first. The bottleneck is rarely where you think.
Profiling Tool Selection
What are you measuring?
├─ Slow UI / frame drops
│ └─ Time Profiler + View Debugger
│ Find expensive work on main thread
│
├─ Memory growth / leaks
│ └─ Allocations + Leaks instruments
│ Track object lifetimes, find cycles
│
├─ Network performance
│ └─ Network instrument + Charles/Proxyman
│ Latency, payload size, request count
│
├─ Disk I/O issues
│ └─ File Activity instrument
│ Excessive reads/writes
│
├─ Battery drain
│ └─ Energy Log instrument
│ CPU wake, location, networking
│
└─ GPU / rendering
└─ Core Animation instrument
Offscreen rendering, overdraw
SwiftUI View Update Strategy
View is re-rendering too often?
├─ Caused by parent state changes
│ └─ Extract to separate view
│ Child doesn't depend on changing state
│
├─ Complex computed body
│ └─ Cache expensive computations
│ Use ViewModel or memoization
│
├─ List items all updating
│ └─ Check view identity
│ Use stable IDs, not indices
│
├─ Observable causing cascading updates
│ └─ Split into multiple @Published
│ Or use computed properties
│
└─ Animation causing constant redraws
└─ Use drawingGroup() or limit scope
Rasterize stable content
Memory Management Decision
How to fix memory issues?
├─ Steady growth during use
│ └─ Check caches and collections
│ Add eviction, use NSCache
│
├─ Growth tied to navigation
│ └─ Check retain cycles
│ weak self in closures, delegates
│
├─ Large spikes on specific screens
│ └─ Downsample images
│ Load at display size, not full resolution
│
├─ Memory not released after screen dismissal
│ └─ Debug object lifecycle
│ deinit not called = retain cycle
│
└─ Background memory pressure
└─ Respond to didReceiveMemoryWarning
Clear caches, release non-essential data
NEVER Do
View Identity
NEVER use indices as identifiers:
// ❌ Identity changes when array mutates
List(items.indices, id: \.self) { index in
ItemRow(item: items[index])
}
// Insert at index 0 → all views recreated!
// ✅ Use stable identifiers
List(items) { item in
ItemRow(item: item)
.id(item.id) // Stable across mutations
}
NEVER compute expensive values in body:
// ❌ Called on every render
var body: some View {
let sortedItems = items.sorted { $0.date > $1.date } // O(n log n) per render!
let filtered = sortedItems.filter { $0.isActive }
List(filtered) { item in
ItemRow(item: item)
}
}
// ✅ Compute in ViewModel or use computed property
@MainActor
class ViewModel: ObservableObject {
@Published var items: [Item] = []
var displayItems: [Item] {
items.filter(\.isActive).sorted { $0.date > $1.date }
}
}
State Management
NEVER use @StateObject for passed objects:
// ❌ Creates new instance on every parent update
struct ChildView: View {
@StateObject var viewModel: ChildViewModel // Wrong!
var body: some View { ... }
}
// ✅ Use @ObservedObject for passed objects
struct ChildView: View {
@ObservedObject var viewModel: ChildViewModel // Parent owns it
var body: some View { ... }
}
NEVER make everything @Published:
// ❌ Every property change triggers view updates
class ViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var internalCache: [String: Data] = [:] // UI doesn't need this!
@Published var isProcessing = false // Maybe internal only
}
// ✅ Only publish what UI observes
class ViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var isLoading = false
private var internalCache: [String: Data] = [:] // Not @Published
private var isProcessing = false // Private state
}
Memory Leaks
NEVER capture self strongly in escaping closures:
// ❌ Retain cycle — never deallocates
class ViewModel {
var timer: Timer?
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.tick() // Strong capture!
}
}
}
// ✅ Weak capture + invalidation
class ViewModel {
var timer: Timer?
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.tick()
}
}
deinit {
timer?.invalidate()
}
}
NEVER forget to remove observers:
// ❌ Leaks observer and potentially self
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(handleNotification),
name: .userLoggedIn,
object: nil
)
// Never removed!
}
}
// ✅ Remove in deinit or use modern API
class ViewController: UIViewController {
private var observer: NSObjectProtocol?
override func viewDidLoad() {
super.viewDidLoad()
observer = NotificationCenter.default.addObserver(
forName: .userLoggedIn,
object: nil,
queue: .main
) { [weak self] _ in
self?.handleNotification()
}
}
deinit {
if let observer { NotificationCenter.default.removeObserver(observer) }
}
}
Image Loading
NEVER load full resolution for thumbnails:
// ❌ 4000×3000 image for 80×80 thumbnail
let image = UIImage(contentsOfFile: path) // Full resolution in memory!
imageView.image = image
// ✅ Downsample to display size
func downsampledImage(at url: URL, to size: CGSize) -> UIImage? {
let options: [CFString: Any] = [
kCGImageSourceShouldCache: false,
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height) * UIScreen.main.scale
]
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
return nil
}
return UIImage(cgImage: cgImage)
}
NEVER cache images without limits:
// ❌ Unbounded memory growth
class ImageLoader {
private var cache: [URL: UIImage] = [:] // Grows forever!
func image(for url: URL) -> UIImage? {
if let cached = cache[url] { return cached }
let image = loadImage(url)
cache[url] = image // Never evicted
return image
}
}
// ✅ Use NSCache with limits
class ImageLoader {
private let cache = NSCache<NSURL, UIImage>()
init() {
cache.countLimit = 100
cache.totalCostLimit = 50 * 1024 * 1024 // 50 MB
}
func image(for url: URL) -> UIImage? {
if let cached = cache.object(forKey: url as NSURL) { return cached }
guard let image = loadImage(url) else { return nil }
cache.setObject(image, forKey: url as NSURL, cost: image.jpegData(compressionQuality: 1)?.count ?? 0)
return image
}
}
Heavy Operations
NEVER do heavy work on main thread:
// ❌ UI frozen during processing
func loadData() {
let data = try! Data(contentsOf: largeFileURL) // Blocks main thread!
let parsed = parseData(data) // Still blocking!
self.items = parsed
}
// ✅ Use background thread, update on main
func loadData() async {
let items = await Task.detached(priority: .userInitiated) {
let data = try! Data(contentsOf: largeFileURL)
return parseData(data)
}.value
await MainActor.run {
self.items = items
}
}
Essential Patterns
Efficient List View
struct EfficientListView: View {
let items: [Item]
var body: some View {
ScrollView {
LazyVStack(spacing: 12) { // Lazy = on-demand creation
ForEach(items) { item in
ItemRow(item: item)
.id(item.id) // Stable identity
}
}
}
}
}
// Equatable row prevents unnecessary updates
struct ItemRow: View, Equatable {
let item: Item
var body: some View {
HStack {
AsyncImage(url: item.imageURL) { image in
image.resizable().aspectRatio(contentMode: .fill)
} placeholder: {
Color.gray.opacity(0.3)
}
.frame(width: 60, height: 60)
.clipShape(RoundedRectangle(cornerRadius: 8))
VStack(alignment: .leading) {
Text(item.title).font(.headline)
Text(item.subtitle).font(.caption).foregroundColor(.secondary)
}
}
}
static func == (lhs: ItemRow, rhs: ItemRow) -> Bool {
lhs.item.id == rhs.item.id &&
lhs.item.title == rhs.item.title &&
lhs.item.subtitle == rhs.item.subtitle
}
}
Memory-Safe ViewModel
@MainActor
final class ViewModel: ObservableObject {
@Published private(set) var items: [Item] = []
@Published private(set) var isLoading = false
private var cancellables = Set<AnyCancellable>()
private var loadTask: Task<Void, Never>?
func load() {
loadTask?.cancel() // Cancel previous
loadTask = Task {
guard !Task.isCancelled else { return }
isLoading = true
defer { isLoading = false }
do {
let items = try await API.fetchItems()
guard !Task.isCancelled else { return }
self.items = items
} catch {
// Handle error
}
}
}
deinit {
loadTask?.cancel()
cancellables.removeAll()
}
}
Debounced Search
@MainActor
final class SearchViewModel: ObservableObject {
@Published var searchText = ""
@Published private(set) var results: [Item] = []
private var searchTask: Task<Void, Never>?
init() {
// Debounce search
$searchText
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.removeDuplicates()
.sink { [weak self] text in
self?.performSearch(text)
}
.store(in: &cancellables)
}
private func performSearch(_ query: String) {
searchTask?.cancel()
guard !query.isEmpty else {
results = []
return
}
searchTask = Task {
do {
let results = try await API.search(query: query)
guard !Task.isCancelled else { return }
self.results = results
} catch {
// Handle error
}
}
}
}
Quick Reference
Instruments Selection
| Issue | Instrument | What to Look For |
|---|---|---|
| Slow UI | Time Profiler | Heavy main thread work |
| Memory leak | Leaks | Leaked objects |
| Memory growth | Allocations | Growing categories |
| Battery | Energy Log | Wake frequency |
| Network | Network | Request count, size |
| Disk | File Activity | Excessive I/O |
| GPU | Core Animation | Offscreen renders |
SwiftUI Performance Checklist
| Issue | Solution |
|---|---|
| Slow list scrolling | Use LazyVStack/LazyVGrid |
| All items re-render | Stable IDs, Equatable rows |
| Heavy body computation | Move to ViewModel |
| Cascading @Published updates | Split or use computed |
| Animation jank | Use drawingGroup() |
Memory Management
| Pattern | Prevent Issue |
|---|---|
| [weak self] in closures | Retain cycles |
| Timer.invalidate() in deinit | Timer leaks |
| Remove observers in deinit | Observer leaks |
| NSCache with limits | Unbounded cache growth |
| Image downsampling | Memory spikes |
os_signpost for Custom Profiling
import os.signpost
let log = OSLog(subsystem: "com.app", category: .pointsOfInterest)
os_signpost(.begin, log: log, name: "DataProcessing")
// Expensive work
os_signpost(.end, log: log, name: "DataProcessing")
Red Flags
| Smell | Problem | Fix |
|---|---|---|
| Indices as List IDs | Views recreated on mutation | Use stable identifiers |
| Expensive body computation | Runs every render | Move to ViewModel |
| @StateObject for passed object | Creates new instance | Use @ObservedObject |
| Strong self in Timer/closure | Retain cycle | Use [weak self] |
| Full-res images for thumbnails | Memory explosion | Downsample to display size |
| Unbounded dictionary cache | Memory growth | Use NSCache with limits |
| Heavy work without Task.detached | Blocks main thread | Use background priority |
More from kaakati/rails-enterprise-dev
flutter conventions & best practices
Dart 3.x and Flutter 3.x conventions, naming patterns, code organization, null safety, and async/await best practices
55getx state management patterns
GetX controllers, reactive state, dependency injection, bindings, navigation, and best practices
52tailadmin ui patterns
TailAdmin dashboard UI framework patterns and Tailwind CSS classes. ALWAYS use this skill when: (1) Building any dashboard or admin panel interface, (2) Creating data tables, cards, charts, or metrics displays, (3) Implementing forms, buttons, alerts, or modals, (4) Building navigation (sidebar, header, breadcrumbs), (5) Any UI work that should follow TailAdmin design. This skill REQUIRES fetching from the official GitHub repository to ensure accurate class usage - NEVER invent classes.
39mvvm-architecture
Expert MVVM decisions for iOS/tvOS: choosing between ViewModel patterns (state enum vs published properties vs Combine), service layer boundaries, dependency injection strategies, and testing approaches. Use when designing ViewModel architecture, debugging data flow issues, or deciding where business logic belongs. Trigger keywords: MVVM, ViewModel, ObservableObject, @StateObject, service layer, dependency injection, unit test, mock, architecture
36rails localization (i18n) - english & arabic
Comprehensive internationalization skill for Ruby on Rails applications with proper English and Arabic translations, RTL support, pluralization rules, date/time formatting, and culturally appropriate content adaptation.
34rspec testing patterns
Complete guide to testing Ruby on Rails applications with RSpec. Use this skill when writing unit tests, integration tests, system tests, or when setting up test infrastructure including factories, shared examples, and mocking strategies.
31