知阅百微 见微知著

SwiftUI async rendering is not just a generic "move drawing to a background thread" switch. In OpenSwiftUI's UIKit hosting path, async rendering is only available when the view graph can update outputs and render the display list without touching main-thread-only state.

The rough shape is:

  1. A display link drives the render loop.
  2. The hosting view decides whether the current pass can use the async renderer.
  3. The ViewGraph.updateOutputsAsync method attempts to update outputs on the async thread.
  4. If the async pass succeeds, the display list is rendered there.
  5. If a requirement is not met, rendering falls back to the main thread.

One surprising requirement is hostPreferenceValues. It is set up when the hierarchy contains dynamic containers such as ForEach, conditional branches, or optional view unwrapping. Without those structures, the graph may have no host preferences to anchor an async update, so allowsAsyncUpdate() returns false.

That means two views that look equally simple in source can behave differently from the renderer's perspective:

struct CanUseAsyncRender: View {
    @State private var items = [6]

    var body: some View {
        VStack {
            ForEach(items, id: \.self) { value in
                Color.blue.opacity(Double(value) / 6.0)
                    .frame(height: 50)
                    .transition(.slide)
            }
        }
        .animation(.easeInOut(duration: 2), value: items)
    }
}

ForEach
creates dynamic structure in the view graph, which gives the renderer the bookkeeping it needs for this async update path.

struct StaysOnMainRenderPath: View {
    @State private var showRed = false

    var body: some View {
        Color(showRed ? .red : .blue)
    }
}

This second example changes a value inside one view. There is no dynamic container boundary, so it is less likely to satisfy the async rendering path's requirements.

Debugging Angle

When a SwiftUI animation unexpectedly stays expensive on the main thread, I do not stop at the animation curve or drawing cost. The view graph shape matters too. Dynamic containers, preference propagation, pending transactions, and main-thread-only attributes can all decide whether async rendering is available.

In OpenSwiftUI, OPENSWIFTUI_PRINT_TREE=1 is a useful way to inspect the display list structure while debugging this class of issue.