Instruments integration and performance analysis workflows for iOS apps. Use when profiling CPU usage, memory allocation, network activity, or energy consumption. Covers Time Profiler, Allocations, Leaks, Network instruments, and performance optimization strategies.
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Comprehensive guide to iOS performance analysis and optimization
Performance profiling is the systematic analysis of an iOS app's runtime behavior to identify bottlenecks, memory issues, network inefficiencies, and energy consumption patterns. This Skill guides you through using Instruments templates, interpreting profiling data, and applying optimization strategies.
Key Tools:
Use performance profiling when:
App Performance Issues
Memory Problems
Network Inefficiencies
Energy Consumption
Pre-Release Optimization
Time Profiler
Allocations
Leaks
Network
Energy Log
Core Animation
Sampling vs Tracing
Sampling (Time Profiler):
Tracing (Most other instruments):
Launch Time
Scrolling Performance
Memory Footprint
Network Efficiency
Energy Impact
Goal: Identify CPU-intensive operations and optimize hot paths
Step 1: Build for Profiling
{
"operation": "build",
"scheme": "MyApp",
"configuration": "Release",
"destination": "platform=iOS,id=<device-udid>",
"options": {
"archive_for_profiling": true
}
}
Why Release Configuration?
Device vs Simulator:
Step 2: Profile with Instruments
# Launch Instruments with Time Profiler template
instruments -t "Time Profiler" \
-D /path/to/trace.trace \
-w <device-udid> \
com.example.MyApp
Manual Approach:
Step 3: Analyze Call Tree
Call Tree Settings:
Interpret Results:
Red Flags:
Step 4: Optimize
Common Optimizations:
Verification:
Goal: Identify memory leaks and reduce memory footprint
Step 1: Build for Profiling
{
"operation": "build",
"scheme": "MyApp",
"configuration": "Debug",
"destination": "platform=iOS Simulator,name=iPhone 15",
"options": {
"enable_memory_debugging": true
}
}
Note: Use Debug for better stack traces, Simulator acceptable for memory profiling
Step 2: Profile with Allocations
# Launch Instruments with Allocations template
instruments -t "Allocations" \
-D /path/to/allocations.trace \
-w <simulator-udid> \
com.example.MyApp
Step 3: Identify Memory Growth
Heap Growth Analysis:
Expected: Minimal growth after repeated operations Red Flag: Continuous growth with each iteration
Step 4: Find Leaks
Switch to Leaks Instrument:
instruments -t "Leaks" \
-D /path/to/leaks.trace \
-w <simulator-udid> \
com.example.MyApp
Interpret Results:
Step 5: Debug Retain Cycles
Memory Graph Debugger:
Common Patterns:
Solutions:
[weak self] in closuresweakGoal: Optimize network requests and reduce data usage
Step 1: Profile with Network Instrument
instruments -t "Network" \
-D /path/to/network.trace \
-w <device-udid> \
com.example.MyApp
Step 2: Analyze Network Activity
Key Metrics:
Red Flags:
Step 3: Optimize Requests
Batching:
// Before: 10 separate requests
for item in items {
fetchDetails(for: item)
}
// After: 1 batched request
fetchDetails(for: items)
Pagination:
// Fetch 20 items at a time
func fetchItems(page: Int, pageSize: Int = 20) {
let offset = page * pageSize
api.fetch(limit: pageSize, offset: offset)
}
Caching:
// Use URLCache or custom cache
let cache = URLCache.shared
cache.diskCapacity = 50 * 1024 * 1024 // 50MB
Compression:
// Enable gzip compression
request.setValue("gzip", forHTTPHeaderField: "Accept-Encoding")
Goal: Reduce battery drain and thermal impact
Step 1: Profile with Energy Log
instruments -t "Energy Log" \
-D /path/to/energy.trace \
-w <device-udid> \
com.example.MyApp
Step 2: Analyze Energy Impact
Energy Sources:
Energy Levels:
Target: Stay in "Low" or "Medium" most of the time
Step 3: Optimize Energy Usage
CPU Optimization:
Network Optimization:
Display Optimization:
Location Optimization:
Measurement Approach:
instruments -t "App Launch" \
-D /path/to/launch.trace \
-w <device-udid> \
com.example.MyApp
Optimization Strategies:
Optimization Strategies:
Target: < 400ms total launch time on device
Measurement Approach:
instruments -t "Core Animation" \
-D /path/to/scroll.trace \
-w <device-udid> \
com.example.MyApp
Check Frame Rate
Identify Frame Drops
Optimization Strategies:
Cell Reuse:
// Proper cell reuse
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
configure(cell: cell, with: data[indexPath.row])
return cell
}
Image Optimization:
// Downsize images to display size
let size = imageView.bounds.size
let downsizedImage = image.resized(to: size)
imageView.image = downsizedImage
Layout Caching:
// Cache calculated heights
private var heightCache: [IndexPath: CGFloat] = [:]
func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = heightCache[indexPath] {
return height
}
let height = calculateHeight(for: indexPath)
heightCache[indexPath] = height
return height
}
Off-Main-Thread Work:
// Move image processing off main thread
DispatchQueue.global(qos: .userInitiated).async {
let processedImage = self.processImage(image)
DispatchQueue.main.async {
self.imageView.image = processedImage
}
}
Systematic Approach:
Identify Suspect Feature
Create Reproduction Steps
1. Launch app
2. Open profile view
3. Close profile view
4. Repeat 10 times
Profile with Leaks
Analyze Leak Origin
Identify Leak Pattern
[weak self] missingweakFix and Verify
Analysis Workflow:
Baseline Measurement
Identify Inefficiencies
Optimization Strategies
Request Batching:
// Batch multiple IDs into single request
func fetchUsers(ids: [String]) {
let batchedIDs = ids.joined(separator: ",")
api.get("/users?ids=\(batchedIDs)")
}
Response Pagination:
// Implement cursor-based pagination
func fetchFeed(cursor: String? = nil, limit: Int = 20) {
var params = ["limit": limit]
if let cursor = cursor {
params["cursor"] = cursor
}
api.get("/feed", parameters: params)
}
Intelligent Caching:
// Cache with expiration
class APICache {
private var cache: [String: CachedResponse] = [:]
func get(url: String) -> Data? {
guard let cached = cache[url],
!cached.isExpired else { return nil }
return cached.data
}
func set(url: String, data: Data, ttl: TimeInterval = 300) {
cache[url] = CachedResponse(data: data, expiry: Date() + ttl)
}
}
Request Coalescing:
// Prevent duplicate in-flight requests
class RequestCoalescer {
private var inFlightRequests: [String: Task<Data, Error>] = [:]
func request(url: String) async throws -> Data {
if let existing = inFlightRequests[url] {
return try await existing.value
}
let task = Task {
let data = try await performRequest(url)
inFlightRequests[url] = nil
return data
}
inFlightRequests[url] = task
return try await task.value
}
}
Understanding Call Tree Structure:
Total Time | Self Time | Symbol
-----------|-----------|--------
1000ms | 10ms | -[UITableView reloadData]
800ms | 50ms | └─ -[MyCell configure]
700ms | 700ms | └─ -[ImageProcessor processImage]
Reading:
reloadData took 1000ms totalreloadData itselfconfigure (called from reloadData)processImage (called from configure)Optimization Target: processImage (700ms self time)
Call Tree Filters:
Separate by Thread:
Hide System Libraries:
Flatten Recursion:
Show Obj-C Only / Swift Only:
Visual Representation:
Interpretation:
Example:
[ main ] ← 100% of time
[ viewDidLoad ][ updateUI ] ← 50% each
[loadData][parseJSON] [layout][render] ← Breakdown
Optimization Strategy:
Workflow:
Capture Memory Graph
Filter View
Inspect Object
Trace Retain Cycle
weak should be usedExample Cycle:
ViewController → (strong) Closure → (strong) ViewController
Fix:
// Before (leak)
viewModel.onUpdate = {
self.updateUI()
}
// After (no leak)
viewModel.onUpdate = { [weak self] in
self?.updateUI()
}
Continuous Performance Testing:
Establish Baseline
Automated Performance Tests
func testLaunchPerformance() throws {
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
func testScrollPerformance() throws {
let app = XCUIApplication()
app.launch()
measure(metrics: [XCTOSSignpostMetric.scrollDecelerationMetric]) {
app.tables.firstMatch.swipeUp()
}
}
CI Integration
Regression Analysis
Baseline Format:
{
"launch_time_ms": 350,
"memory_mb": 120,
"scroll_fps": 59,
"thresholds": {
"launch_time_ms": 450,
"memory_mb": 150,
"scroll_fps": 55
}
}
Symptoms:
Common Causes:
1. Main Thread Blocking
// Problem: Heavy work on main thread
DispatchQueue.main.async {
let result = expensiveCalculation() // Blocks UI
updateUI(with: result)
}
// Solution: Move work off main thread
DispatchQueue.global(qos: .userInitiated).async {
let result = expensiveCalculation()
DispatchQueue.main.async {
updateUI(with: result)
}
}
2. Polling/Tight Loops
// Problem: Constant polling
Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { _ in
checkForUpdates() // Called 100 times per second!
}
// Solution: Reasonable interval or event-driven
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
checkForUpdates() // Called once per second
}
3. Inefficient Algorithms
// Problem: O(n²) complexity
for item in items {
for other in items {
compare(item, other) // n² comparisons
}
}
// Solution: O(n log n) or O(n)
let sorted = items.sorted()
for (item, other) in zip(sorted, sorted.dropFirst()) {
compare(item, other) // n comparisons
}
Symptoms:
Common Patterns:
1. Unbounded Cache Growth
// Problem: Cache grows indefinitely
class ImageCache {
private var cache: [URL: UIImage] = [:] // Never clears!
}
// Solution: Use NSCache (auto-eviction)
class ImageCache {
private let cache = NSCache<NSURL, UIImage>()
init() {
cache.countLimit = 100 // Max 100 images
}
}
2. Event Listener Accumulation
// Problem: Listeners never removed
override func viewWillAppear(_ animated: Bool) {
NotificationCenter.default.addObserver(/* ... */) // Added every time!
}
// Solution: Remove in viewWillDisappear
override func viewWillDisappear(_ animated: Bool) {
NotificationCenter.default.removeObserver(self)
}
3. Large Object Retention
// Problem: Keeping large objects in memory
class ViewController: UIViewController {
var cachedImage: UIImage? // Large image retained
}
// Solution: Cache only when needed, clear when done
class ViewController: UIViewController {
private var cachedImage: UIImage?
override func didReceiveMemoryWarning() {
cachedImage = nil // Release when memory pressure
}
}
Symptoms:
Common Issues:
1. Waterfall Requests
// Problem: Sequential dependent requests
func loadProfile() async {
let user = await fetchUser() // Wait...
let posts = await fetchPosts(for: user) // Wait...
let comments = await fetchComments(for: posts) // Wait...
}
// Solution: Parallel independent requests
func loadProfile() async {
async let user = fetchUser()
async let followers = fetchFollowers()
async let settings = fetchSettings()
let (u, f, s) = await (user, followers, settings)
}
2. No Request Deduplication
// Problem: Same request made multiple times
func loadData() {
fetchUsers() // Request 1
fetchUsers() // Request 2 (redundant!)
}
// Solution: Deduplicate requests
class APIClient {
private var pendingRequests: [String: Task<Data, Error>] = [:]
func fetch(url: String) async throws -> Data {
if let pending = pendingRequests[url] {
return try await pending.value // Reuse
}
let task = Task { try await URLSession.shared.data(from: URL(string: url)!) }
pendingRequests[url] = task
defer { pendingRequests[url] = nil }
return try await task.value
}
}
3. Large Uncompressed Payloads
// Problem: Sending/receiving uncompressed data
URLSession.shared.dataTask(with: url) // Default: no compression
// Solution: Enable compression
var request = URLRequest(url: url)
request.setValue("gzip, deflate", forHTTPHeaderField: "Accept-Encoding")
URLSession.shared.dataTask(with: request)
Symptoms:
Common Causes:
1. Excessive Background Activity
// Problem: Constant background work
func applicationDidEnterBackground(_ application: UIApplication) {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
syncData() // Drains battery in background
}
}
// Solution: Use background tasks properly
func applicationDidEnterBackground(_ application: UIApplication) {
let taskID = application.beginBackgroundTask {
// Task expired, clean up
}
syncData {
application.endBackgroundTask(taskID)
}
}
2. Continuous Location Updates
// Problem: Always requesting location
locationManager.startUpdatingLocation() // Continuous GPS drain
// Solution: Use appropriate accuracy
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.distanceFilter = 100 // Update every 100m
locationManager.startMonitoringSignificantLocationChanges() // Low power mode
3. Rendering Offscreen Content
// Problem: Animating hidden views
override func viewDidDisappear(_ animated: Bool) {
// Animations keep running! Wastes CPU/battery
}
// Solution: Pause animations when offscreen
override func viewDidDisappear(_ animated: Bool) {
animationView.layer.pauseAnimations()
}
extension CALayer {
func pauseAnimations() {
let pausedTime = convertTime(CACurrentMediaTime(), from: nil)
speed = 0.0
timeOffset = pausedTime
}
}
Always profile on device for:
Simulator acceptable for:
Recommendation: Develop on simulator, validate on device, profile on device.
Why Profile Release Builds:
Debug vs Release Differences:
How to Profile Release:
Warning: Some crashes only happen in release due to optimizations.
Development Workflow:
CI Integration:
# .github/workflows/performance.yml
name: Performance Tests
on: [pull_request]
jobs:
performance:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Run performance tests
run: xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 15'
- name: Check for regressions
run: ./scripts/check_performance_baseline.sh
Automated Performance Tests:
class PerformanceTests: XCTestCase {
func testLaunchPerformance() {
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
func testMemoryUsage() {
let app = XCUIApplication()
app.launch()
measure(metrics: [XCTMemoryMetric()]) {
// Perform memory-intensive operations
for _ in 0..<100 {
app.buttons["Load"].tap()
}
}
}
}
Establish Budgets:
// PerformanceBudgets.swift
enum PerformanceBudget {
static let launchTime: TimeInterval = 0.4 // 400ms
static let memoryFootprint: Int = 150 * 1024 * 1024 // 150MB
static let scrollFrameTime: TimeInterval = 0.0167 // 16.67ms (60 FPS)
static let networkRequestTimeout: TimeInterval = 5.0 // 5s
}
// Enforce in tests
func testLaunchBudget() {
let launchTime = measureLaunchTime()
XCTAssertLessThan(launchTime, PerformanceBudget.launchTime,
"Launch time exceeded budget: \(launchTime)s > \(PerformanceBudget.launchTime)s")
}
Budget Categories:
Launch Time:
Memory:
Frame Rate:
Network:
Monitor Budgets:
This Skill integrates with xcode-workflows Skill:
Build for Profiling:
{
"operation": "build",
"scheme": "MyApp",
"configuration": "Release",
"destination": "platform=iOS,id=<device-udid>",
"options": {
"clean_before_build": true
}
}
Archive for Profiling:
xcodebuild archive \
-scheme MyApp \
-configuration Release \
-archivePath ./build/MyApp.xcarchive
Export for Profiling:
xcodebuild -exportArchive \
-archivePath ./build/MyApp.xcarchive \
-exportPath ./build \
-exportOptionsPlist ExportOptions.plist
xc://operations/xcode: Xcodebuild operations for profiling buildsxc://reference/instruments: Complete Instruments template referencexc://reference/performance-metrics: Key performance indicators and targetsTip: Profile on device, use Release configuration, focus on user-impacting metrics first.