# Profiling with the SwiftUI Instrument

Based on the WWDC25 session "Optimize SwiftUI performance with Instruments".
The SwiftUI Instrument described here ships with Instruments 26.

## When It Is the Right Tool

Reach for it when the symptom points at view updates, layout, rendering or
state spreading too far:

- scrolling stutters,
- a push to a new screen pauses first,
- an animation is uneven although nothing obviously blocks the main thread,
- a timer or observable model triggers far more updates than expected.

When the whole app freezes and nothing points at the UI, start with Hangs or
Time Profiler and come to the SwiftUI lanes afterwards.

## Capture Setup

- Release build on a real device.
- A sequence you can repeat exactly.
- As little debug logging as possible.
- Enough repetitions that a pattern shows.

A good default set: the SwiftUI Instrument plus Time Profiler, and Hangs or
Animation Hitches when the symptom calls for it.

## The SwiftUI Lanes

| Lane | What it tells you |
|------|-------------------|
| Update Groups | Coarse buckets of work. Shows bursts even when no single update is slow. |
| Long View Body Updates | `body` evaluations that took too long. Orange is worth a look, red is clearly too slow. |
| Long Platform View Updates | Time in hosted UIKit or AppKit views: `UIViewRepresentable`, `UIViewControllerRepresentable`, heavy list or table bridging, embedded media or web views. |
| Other Long Updates | Text layout, geometry, list diffing and update coordination. |

## Workflow: One Update Is Too Slow

1. Pick a single red or orange update, not a long stretch of timeline.
2. Set the inspection range to that update so Time Profiler shows the same
   interval.
3. Read the call tree or flame graph. Typical findings: a formatter being
   created, sorting or filtering, image decoding, string building, model
   construction, a synchronous read from storage.
4. Move that work out of `body`:

```swift
// Before: runs on every evaluation.
Text(playlist.tracks.sorted { $0.position < $1.position }
    .map(\.title).joined(separator: ", "))

// After: the model keeps the joined string current.
Text(playlist.trackSummary)
```

5. Record again. If long updates remain, the work probably moved into a child
   view or reaches the view through another dependency.

## Workflow: Too Many Updates

A screen can feel slow because it updates constantly, even if every single
update is cheap.

- Start with Update Groups: look for long active stretches full of updates with
  no red spikes.
- Look at counts, not only durations. Cheap work done 200 times can feel worse
  than one moderately expensive update.
- Open the Cause and Effect Graph to see what changed, which dependency fired
  and why the update spread.

Typical fixes:
- extract subviews that read only the state they show,
- replace a broad shared model with narrower derived state,
- stop passing fast-changing data through the environment,
- split a large `@Observable` model whose unrelated properties change together.

## Cause and Effect Graph

Use it when backtraces point in the wrong direction; in a declarative UI the
code that did the work is often far from the code that caused it. Good uses:

- changing the selection also updates an unrelated sidebar,
- a timer invalidates an entire list,
- a geometry change ripples through many subviews,
- a global favourites model makes every row re-evaluate.

## Hot Spots and How to Fix Them

- Never build `DateFormatter`, `NumberFormatter` or `MeasurementFormatter` in
  `body` or per row.
- Filter collections before rendering; do not route raw geometry through a
  broadly observed model.
- Unstable identity and a row count that changes between passes make lists
  and tables expensive.
- Check representables for configuration repeated on every
  `updateUIView` or `updateUIViewController` call.

Cache presentation values in a model:

```swift
@Observable
final class RunSummary {
    private(set) var distanceText = ""

    func update(metres: Double, using formatter: MeasurementFormatter) {
        let distance = Measurement<UnitLength>(value: metres, unit: .meters)
        distanceText = formatter.string(from: distance)
    }
}
```

- Scope observable dependencies so a row reads only its own state.
- Make representable updates idempotent: touch the hosted view only when an
  input actually changed.
- Tame noisy geometry or timer signals with a threshold, debouncing or
  coalescing.

## Measuring Again

- Record the same flow again.
- Compare how many updates happen, not only how long they take.
- Confirm there are fewer hitches.
- Confirm no new Long Platform View Updates appeared.
- Check in Release on a device.

If one screen gets faster while updates rise somewhere else, keep following the
dependency graph until the fan-out really goes down.
