SwifUI Performance With Rapid UI Updates

The code below is a test to trigger UI updates every 30 seconds. I'm trying to keep most work off main and only push to main once I have the string (which is cached). Why is updating SwiftUI 30 times per second so expensive? This code causes 10% CPU on my M4 Mac, but comment out the following line:

Text(model.timeString)

and it's 0% CPU. The reason why I think I have too much work on main is because of this from instruments. But I'm no instruments expert.

import SwiftUI
import UniformTypeIdentifiers

@main
struct RapidUIUpdateTestApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: RapidUIUpdateTestDocument()) { file in
            ContentView(document: file.$document)
        }
    }
}


struct ContentView: View {
    @Binding var document: RapidUIUpdateTestDocument
    @State private var model = PlayerModel()
    var body: some View {
        VStack(spacing: 16) {
            Text(model.timeString) // only this changes
                .font(.system(size: 44, weight: .semibold, design: .monospaced))
                .transaction { $0.animation = nil } // no implicit animations
            HStack {
                Button(model.running ? "Pause" : "Play") {
                    model.running ? model.pause() : model.start()
                }
                Button("Reset") { model.seek(0) }
                Stepper("FPS: \(Int(model.fps))", value: $model.fps, in: 10...120, step: 1)
                    .onChange(of: model.fps) { _, _ in model.applyFPS() }
            }
        }
        .padding()
        .onAppear { model.start() }
        .onDisappear { model.stop() }
    }
}

@Observable
final class PlayerModel {
    // Publish ONE value to minimize invalidations
    var timeString: String = "0.000 s"
    var fps: Double = 30
    var running = false
    

    private var formatter: NumberFormatter = {
        let f = NumberFormatter()
        f.minimumFractionDigits = 3
        f.maximumFractionDigits = 3
        return f
    }()

    @ObservationIgnored private let q = DispatchQueue(label: "tc.timer", qos: .userInteractive)
    @ObservationIgnored private var timer: DispatchSourceTimer?
    @ObservationIgnored private var startHost: UInt64 = 0
    @ObservationIgnored private var pausedAt: Double = 0
    @ObservationIgnored private var lastFrame: Int = -1

    // cache timebase once
    private static let secsPerTick: Double = {
        var info = mach_timebase_info_data_t()
        mach_timebase_info(&info)
        return Double(info.numer) / Double(info.denom) / 1_000_000_000.0
    }()

    func start() {
        guard timer == nil else { running = true; return }
        
        let desiredUIFPS: Double = 30        // or 60, 24, etc.
        let periodNs = UInt64(1_000_000_000 / desiredUIFPS)
        
        running = true
        startHost = mach_absolute_time()

        let t = DispatchSource.makeTimerSource(queue: q)
        // ~30 fps, with leeway to let the kernel coalesce wakeups
        t.schedule(
            deadline: .now(),
            repeating: .nanoseconds(Int(periodNs)), // 33_333_333 ns ≈ 30 fps
            leeway: .milliseconds(30)                // allow coalescing
        )
        t.setEventHandler { [weak self] in self?.tick() }
        timer = t
        t.resume()
    }

    func pause() {
        guard running else { return }
        pausedAt = now()
        running = false
    }

    func stop() {
        timer?.cancel()
        timer = nil
        running = false
        pausedAt = 0
        lastFrame = -1
    }

    func seek(_ seconds: Double) {
        pausedAt = max(0, seconds)
        startHost = mach_absolute_time()
        lastFrame = -1 // force next UI update
    }

    func applyFPS() { lastFrame = -1 } // next tick will refresh string

    // MARK: - Tick on background queue
    private func tick() {
        let s = now()
     
        let str = formatter.string(from: s as NSNumber) ?? String(format: "%.3f", s)
        let display = "\(str) s"

        DispatchQueue.main.async { [weak self] in
            self?.timeString = display
        }
    }

    private func now() -> Double {
        guard running else { return pausedAt }
        let delta = mach_absolute_time() &- startHost
        return pausedAt + Double(delta) * Self.secsPerTick
    }
}

nonisolated struct RapidUIUpdateTestDocument: FileDocument {
    var text: String

    init(text: String = "Hello, world!") {
        self.text = text
    }

    static let readableContentTypes = [
        UTType(importedAs: "com.example.plain-text")
    ]

    init(configuration: ReadConfiguration) throws {
        guard let data = configuration.file.regularFileContents,
              let string = String(data: data, encoding: .utf8)
        else {
            throw CocoaError(.fileReadCorruptFile)
        }
        text = string
    }
    
    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        let data = text.data(using: .utf8)!
        return .init(regularFileWithContents: data)
    }
}
SwifUI Performance With Rapid UI Updates
 
 
Q