Mobile Engineering

Swift Observation performance improvements: audit over-invalidation and unobserved writes before the next toolchain drop

Swift Observation is getting active performance/model-shape work again. This audit shows what to measure now—over-invalidation and unobserved writes—so you can prove gains later and avoid new regressions.

Swift Observation can still cost you in two predictable ways: your UI re-renders more than it should, and you pay work for writes nobody is watching.

A new Swift Forums pitch (6 October 2026) is explicitly targeting both modelling and performance pain points, including the cost of mutating unobserved properties and the need to better support real-world model shapes. The right move now is to audit your app so you can (a) validate improvements when they land and (b) stop shipping accidental regressions in the meantime. (forums.swift.org)

What changed in the conversation (and why it matters to your roadmap)

Observation shipped with the promise of property-level tracking: only the properties read during tracking should participate in invalidation, which is exactly what SwiftUI wants when it decides whether to re-evaluate a view body. Apple’s WWDC23 material leans on that idea: views shouldn’t be invalidated by properties they didn’t read. (developer.apple.com)

But real systems don’t just “read in body, write on tap”. They have caches, derived state, background refresh, and sync layers. Practitioners have been discussing over-observing patterns (for example: one view uses a single field inside a large nested struct, yet ends up effectively depending on far more than intended) and ways to build derived/cached values with withObservationTracking. (forums.swift.org)

The October 2026 pitch is important because it calls out a specific performance failure mode: mutating an unobserved property on an observed object can be expensive today, and proposes runtime changes (including passing property indices into registrar entry points) so the system can skip work when nobody is observing that property. (forums.swift.org)

Even if you never adopt the future APIs directly, the pitch is a signal: the underlying runtime behaviour may change, and apps that have relied on incidental behaviour (or papered over it with hand-rolled caching) are where regressions usually appear first.

The two audit targets that pay off fastest

This article focuses on the two areas you can measure without guessing:

  • Over-invalidation: “Something changed” causes more view updates / work than necessary.
  • Unobserved writes: You mutate state that is (currently) not being tracked by any consumer, but you still pay meaningful overhead.

They are related, but not the same. Over-invalidation is about too many reads being tracked; unobserved writes are about writes paying costs even when there are no readers.

Audit 1: Find over-invalidation (the common SwiftUI symptom)

Observation’s value in SwiftUI is that it is supposed to be selective: only reads are tracked, and only changes to tracked properties should trigger re-evaluation. (developer.apple.com)

When you see excessive body calls, you should assume your data dependencies are too broad before you assume SwiftUI is “slow”. (That’s also the framing in current developer tooling discussions: the SwiftUI Instrument should be your primary lens, not print debugging.) (github.com)

What over-invalidation looks like in production

Typical failure modes:

  • A large @Observable model is threaded through many views; a mid-level view reads a broad property (or computed property) “for convenience”, which implicitly drags half the model into the dependency set.
  • A computed property reads multiple stored properties, so any of them changing invalidates any view reading the computed property. This is correct behaviour, but often surprises teams when the computed property is used widely.
  • “Derived state” is computed in body (or in computed properties used by body) and re-computed far more often than assumed.

When the toolchain improves invalidation or reduces incidental invalidations, these patterns can still dominate your costs. The audit’s job is to surface them and let you tighten dependencies so you benefit from runtime improvements rather than masking them.

How to measure it (so you can compare later)

Do this on a representative screen, with real navigation and real data volume.

  1. Choose 2–3 flows that are visibly “busy”: lists with sorting/filtering, dashboards with derived metrics, or anything with background refresh.
  2. Record a baseline run and keep it under version control (screen recording plus notes is fine).
  3. Use Instruments’ SwiftUI instrument to capture view body evaluations and identify which views are being re-evaluated most often. (github.com)
  4. For the top offenders, write down: - Which model(s) they depend on. - Which properties they actually need. - Whether the dependency comes from a computed property.

You are not trying to micro-optimise. You are building a dependency map you can revisit after a toolchain update.

What to change (and when the answer is “do not do this”)

Use these rules of thumb:

  • Prefer smaller view models (or smaller observable sub-models) over “one god object”. If a view only needs user.displayName, don’t give it AppState.
  • Avoid reading broad computed properties in leaf views if they read many fields. Either narrow the computed property, or compute it once higher up and pass the result down.
  • Do not “fix” over-invalidation by caching everywhere inside the model unless you can prove it helps and you can prove it stays correct. Hand caches are where correctness bugs hide, and the 2026 pitch explicitly calls out hand caching as error-prone in this space. (forums.swift.org)

Audit 2: Identify unobserved writes (the one the pitch is directly targeting)

Observation isn’t only about SwiftUI. It is also a general mechanism you can use outside SwiftUI via withObservationTracking(_:onChange:), which tracks properties read during the apply closure and triggers onChange when those properties change. Apple’s documentation example is explicit: if you don’t read a property during tracking, changes to it won’t trigger onChange. (developer.apple.com)

The October 6, 2026 pitch zooms in on a different cost: the write path can be expensive even when a property is unobserved, and it proposes runtime changes so mutations of unobserved properties can skip certain registrar work (including lock acquisition). (forums.swift.org)

What an “unobserved write” looks like in real apps

Common scenarios:

  • You have an observable model used by SwiftUI, but you also update “maintenance” fields (last refresh date, internal caches, metrics, precomputed indexes) that are not read by the UI.
  • You batch-update many properties during sync or import, but only a subset are read by active views.
  • You mutate observable state from background pipelines where no tracking is active (no SwiftUI view reading it, no withObservationTracking consumer set up), yet you still pay overhead.

How to measure unobserved writes today

You want two things: frequency and cost.

  1. Find your highest-volume write sites: - Sync/import loops. - WebSocket / live-update handlers. - Timers that update derived state.
  2. For each site, determine whether anything is actively observing those properties during the scenario. - If it is UI-driven, reproduce the write while the relevant screen is not visible. - If it is a background process, reproduce it with the app in a “quiet UI” state.
  3. Profile CPU time and lock contention around those write bursts.

You don’t need a perfect attribution model to get value; you need a repeatable reproduction and a baseline trace.

What to change now (without fighting the framework)

Treat this as an architectural boundary problem:

  • Keep UI-facing observable state separate from internal mutable state.
  • If a field is not meant to drive UI or downstream observers, it probably doesn’t belong on the same @Observable reference type.
  • Consider moving heavy “maintenance” work into:
  • A non-observable helper type.
  • A value type snapshot that is rebuilt on demand.
  • If you must keep it on an observable type, be explicit about whether any consumer should ever observe it. If the answer is “no”, consider removing it from the observable surface.

This is where you get leverage from the pitch’s direction: if the runtime gets better at skipping work for unobserved properties, you want to be able to demonstrate that your app actually has those cases and that they matter.

Derived state and caches: be precise or you will pay twice

The Swift Forums “over-observing” discussion is essentially about derived state: you want a derived value, you want it cached, and you want it to update when the right inputs change — but not trigger a cascade when unrelated inputs change. (forums.swift.org)

A practical policy that tends to hold up:

  • If the derived value is cheap, compute it directly.
  • If it is expensive and used in UI, compute it once per change and store it, but make the dependency set explicit.
  • If it is expensive and not UI-facing, do not put it on your primary observable model.

The “do not do this” version:

  • Do not introduce caching that depends on “I think this changes less often”. Either your cache invalidation is correctly tied to observation, or it’s a future bug.

A lightweight checklist you can run this week

  • Pick 2–3 “hot” screens and capture a baseline in Instruments using the SwiftUI instrument. (github.com)
  • For each screen, identify the top 5 views by body evaluation count.
  • For each of those views:
  • List the exact properties it reads (not the objects it holds).
  • Identify any computed properties and what they transitively read.
  • Find the top 3 write-heavy code paths (sync/import/streaming updates).
  • For each write-heavy path:
  • Note which properties are written.
  • Verify which are actually observed in the scenario.
  • Capture a CPU baseline trace for the write burst.
  • Add one regression test per hot screen: not a benchmark, just a scripted UI flow you can re-run after updating Xcode/Swift.

What to expect when the “next wave” lands

The pitch is not a promise of ship dates, but it does make the intended direction clear: improving how Observation models property mutations and reducing overhead for writes to properties nobody is observing, including new registrar entry points keyed by property indices. (forums.swift.org)

If (when) those improvements ship, teams who did the audit will be able to answer questions quickly:

  • Did our view invalidation counts drop on the same flows?
  • Did our background sync cost drop when UI is idle?
  • Did any of our caches or derived-state constructs behave differently?

And if something regresses, you will have a clear reproduction anchored to “this view is re-evaluating because it reads X” or “this background path writes Y even when no observer exists”, rather than a vague sense that “Observation got slower”.

Where Codeversols can help (briefly, and concretely)

If you want this audited without burning senior engineering time, Codeversols can run the instrumentation pass, produce a dependency map for your hot screens, and propose low-risk refactors that reduce over-invalidation and isolate unobserved writes. We do this kind of work across iOS, backend, and data layers, so the fixes can land where they belong (often not in the view).

Close

Observation is already good enough to ship with, but it is also still evolving. The teams who benefit most from runtime improvements are the ones who can clearly see their current invalidation and write costs.

Run the audit now: capture baselines, narrow dependencies, and separate UI-observable state from internal churn. When the next toolchain lands, you’ll be in a position to take the win — and to prove it.

More From Our Blog

Android 16 (API 36) edge-to-edge is enforced: the window insets contract your UI must satisfy
Mobile Engineering • Oct 16, 2026

Android 16 (API 36) edge-to-edge is enforced: the window insets contract your UI must satisfy

Targeting Android 16 (API 36) removes the practical opt-out from enforced edge-to-edge. The only stable fix is to define a single owner for each inset (status bars, navigation bars, display cutout, IME) and test for double-application in CI.

Read more →
TestFlight ‘Waiting for Review’ for days? Ship anyway with a dual‑lane release pipeline (and what to automate in App Store Connect)
Mobile Engineering • Oct 15, 2026

TestFlight ‘Waiting for Review’ for days? Ship anyway with a dual‑lane release pipeline (and what to automate in App Store Connect)

When TestFlight Beta App Review and App Store Review queues stall, you don’t need to miss your launch date. This playbook describes a dual‑lane pipeline (internal TestFlight vs external TestFlight vs App Store), deterministic rollout rules, and the App Store Connect automation to keep shipping under review latency.

Read more →
Swift 6 strict concurrency default isolation footguns: how teams accidentally @MainActor their whole app (and a CI check to prove it)
iOS / Swift • Oct 14, 2026

Swift 6 strict concurrency default isolation footguns: how teams accidentally @MainActor their whole app (and a CI check to prove it)

Swift 6’s strict concurrency checks get teams over the line, but one build setting can quietly turn large parts of your app into MainActor-only code. Here’s how that happens, how it shows up as UI jank, and a simple CI audit that catches “main-actor creep” early.

Read more →