DEV Community

Sebastien Lato
Sebastien Lato

Posted on

SwiftUI Performance Profiling with Instruments (Practical Guide)

If you don’t profile, you’re guessing.

And in SwiftUI, guessing leads to:

  • random .id() hacks
  • unnecessary EquatableView
  • broken animations
  • mysterious jank
  • premature micro-optimizations

This post is a practical, no-BS guide to profiling SwiftUI apps with Instruments:

  • where to look
  • what matters
  • what to ignore
  • how to interpret results
  • how to fix real issues

This is how you go from “it feels slow” to “I know why.”


🧠 The Core Principle

Measure first. Optimize second.

SwiftUI performance problems are usually:

  • layout thrashing
  • excessive body recomputation
  • main-thread blocking
  • memory churn
  • view identity misuse

Instruments shows all of this.


🧰 The 4 Instruments You Actually Need

Ignore the rest. Start with:

  1. Time Profiler
  2. SwiftUI
  3. Allocations
  4. Leaks

These four cover 95% of SwiftUI issues.


⏱️ 1. Time Profiler — Find the Real Bottleneck

Open:
Xcode → Product → Profile → Time Profiler

Look for:

  • long main-thread blocks
  • repeated calls to the same functions
  • heavy JSON decoding
  • image decoding
  • layout loops

What to watch:

  • body being called excessively
  • ViewGraph updates
  • expensive modifiers
  • synchronous work in .task

🧠 Common Time Profiler Patterns

❌ Pattern: Heavy work in body

var body: some View {
    let data = heavyComputation()
    ...
}
Enter fullscreen mode Exit fullscreen mode

Fix:

@State private var data: Data

.task {
    data = await heavyComputation()
}
Enter fullscreen mode Exit fullscreen mode

❌ Pattern: Synchronous decoding

let image = UIImage(data: data)
Enter fullscreen mode Exit fullscreen mode

Fix:

  • decode off main thread (as covered in #47)

🧬 2. SwiftUI Instrument — View Update Tracing

This is gold.

Enable:

Instruments  SwiftUI
Enter fullscreen mode Exit fullscreen mode

You’ll see:

  • which views re-render
  • how often
  • why

Look for:

  • views updating when nothing changed
  • large subtrees invalidating
  • identity resets
  • unexpected animations

🧠 Red Flags in SwiftUI Instrument

  • Parent view updates → entire screen redraws
  • Small state change → full list invalidates
  • Repeated layout passes
  • Views recreated instead of updated

These usually mean:

  • bad identity
  • wrong state placement
  • environment misuse

🧪 3. Allocations — Memory Churn

Open:

Instruments  Allocations
Enter fullscreen mode Exit fullscreen mode

Look for:

  • rapid object creation
  • spikes on scroll
  • repeated ViewModel creation
  • image churn
  • task churn

This catches:

  • memory leaks
  • over-allocation
  • performance killers

🧠 Classic Allocation Problems

❌ Creating objects in body

var body: some View {
    let formatter = DateFormatter()
    ...
}
Enter fullscreen mode Exit fullscreen mode

Fix:

private let formatter = DateFormatter()
Enter fullscreen mode Exit fullscreen mode

Or inject.


❌ Recreating ViewModels

SomeView(viewModel: ViewModel())
Enter fullscreen mode Exit fullscreen mode

Fix:

@StateObject private var vm = ViewModel()
Enter fullscreen mode Exit fullscreen mode

🧯 4. Leaks — Confirm Deallocation

Open:

Instruments  Leaks
Enter fullscreen mode Exit fullscreen mode

Then:

  • navigate through your app
  • push/pop views
  • open/close features
  • switch tabs
  • open/close windows

Watch:

  • ViewModels
  • services
  • containers

If they don’t deinit — you have a bug.

(See #44 for ownership rules.)


🧭 5. How to Profile a Screen (Step-by-Step)

  1. Open Instruments
  2. Select Time Profiler + SwiftUI
  3. Navigate to the problematic screen
  4. Interact:
  5. scroll
  6. tap
  7. animate
  8. load data
  9. Stop recording
  10. Inspect:
  11. main thread
  12. SwiftUI updates
  13. allocations

Never profile on simulator. Always use a device.


🧠 6. Interpreting SwiftUI Re-Renders

Ask:

  • Which state changed?
  • Why did this view invalidate?
  • Does identity change?
  • Is environment causing this?
  • Is a parent invalidating children?

If you can’t answer, your architecture is wrong.


🧬 7. Layout Thrashing Detection

Symptoms:

  • high CPU on scroll
  • repeated layout passes
  • jerky animations

Instruments shows:

  • repeated calls to layout functions
  • LayoutComputer activity

Fixes:

  • reduce nested stacks
  • avoid GeometryReader abuse
  • avoid preference loops
  • flatten view hierarchy

🧪 8. Animation Profiling

Use:

  • Core Animation
  • Time Profiler

Look for:

  • dropped frames
  • layout during animation
  • heavy modifiers during transitions

Common fixes:

  • move work out of animation blocks
  • avoid layout-changing animations
  • precompute values

🧠 9. Real-World Profiling Checklist

Profile:

  • cold launch
  • scrolling long lists
  • navigation push/pop
  • tab switching
  • deep link entry
  • background → foreground
  • orientation change

These reveal 90% of issues.


❌ 10. Common Anti-Patterns

Avoid:

  • profiling only in debug
  • ignoring Instruments warnings
  • optimizing without evidence
  • blaming SwiftUI blindly
  • using random modifiers to “fix” jank
  • shipping without profiling

Performance is not optional.


🧠 Mental Model

Think:

User Action
  State Change
    View Invalidation
      Layout
        Render
          GPU
Enter fullscreen mode Exit fullscreen mode

Instruments shows you which step is broken.


🚀 Final Thoughts

Instruments turns:

  • “it feels slow” into
  • “this function blocks the main thread for 32ms” That’s power.

Once you use it regularly:

  • architecture improves
  • code quality rises
  • bugs drop
  • confidence grows

Top comments (0)