Build efficient SwiftUI views
Outcome
Produce correct, maintainable SwiftUI views whose bodies finish quickly, update
only for relevant changes, and keep resources within their intended lifetimes.
Treat correctness as a gate. Use evidence proportionate to the change: direct
dependency, identity, work, control-flow, or ownership proof can justify a
low-risk refactor; use profiling to attribute a runtime regression or claim a
measured improvement.
Read references selectively
- Read
references/data-flow-and-diffing.md before choosing state ownership,
passing model data, introducing Equatable, or reasoning about updates.
- Read
references/observation.md before introducing @Observable, migrating
from ObservableObject, or choosing @State, @Bindable, or environment
injection for an observable model.
- Read
references/construction-patterns.md before changing view boundaries,
layout readers, tasks, presentation, or animations.
- Read
references/collections-and-scrolling.md before changing List,
Table, a lazy stack or grid, ForEach, programmatic scrolling, or a
high-frequency feed.
- Read
references/memory-and-resources.md before diagnosing memory growth,
leaks, image footprint, cache retention, or persistence-backed screens.
- Read
references/profiling.md before diagnosing an existing regression or
claiming a performance improvement.
- Read
references/source-notes.md when a recommendation is disputed,
version-sensitive, or based on undocumented behavior.
Repository instructions, supported deployment targets, and established
architecture override generic examples. They do not override the requirement
to preserve correctness and verify performance claims.
Establish the task
- Read the repository's root and nearest local instructions.
- Inspect the deployment targets, Xcode and Swift versions, target platforms,
existing state model, navigation architecture, and test commands.
- Classify the request as:
- new view or screen;
- behavior-preserving refactor;
- measured performance diagnosis;
- performance-focused code review.
- For a runtime-dependent issue, record one reproducible interaction,
representative data volume, affected device and OS, build configuration, and
visible symptom before making a performance claim. Do not require those
runtime artifacts before correcting a direct source violation.
- Classify memory symptoms as transient growth, persistent growth, abandoned
reachable memory, a reference-cycle leak, or an unbounded resource policy
before changing ownership.
- Do not redesign unrelated architecture or add optimization machinery without
a demonstrated need.
Treat a request to create, optimize, fix, or refactor as edit authority within
its named scope. Do not downgrade it to code review because a profiler,
Simulator, physical device, or baseline capture is unavailable.
Use the SwiftUI mental model
- Treat a
View value as a description, not a persistent widget. SwiftUI may
create view values and evaluate body frequently.
- Distinguish
body evaluation, graph reconciliation, platform rendering, and
display presentation. A logged body call is not proof of a rendered frame
or a user-visible regression.
- Treat stored inputs, dynamic properties, environment values, and observable
properties read by the view as dependencies.
- Preserve structural and explicit identity when the same semantic element
should retain state and animate continuously.
- Create boundaries around coherent dependency sets, not arbitrary line counts.
Construct the view
- Define the smallest render contract. Pass a child the values it displays
instead of an entire feature state or broad store when practical.
- Choose each state wrapper by ownership and lifetime using
references/data-flow-and-diffing.md and references/observation.md; never
choose a wrapper because it is rumored to "render less."
- Keep
init and body deterministic and cheap. Move I/O, decoding, large
filtering or sorting, expensive formatting, and business logic into a model
or service. Cache derived results with explicit invalidation.
- Extract a real
struct: View when a subtree needs an independent update
boundary. A computed some View, helper function, closure, Group, or
@ViewBuilder helper does not create that boundary.
- Keep event sources and frequently changing dependencies in the smallest
subtree that needs them.
- Use stable domain identity in
ForEach, List, and Table. Follow
references/collections-and-scrolling.md for row cardinality, lazy
lifetime, prefetching, and high-frequency data.
- Tie asynchronous work to view lifetime with
.task or .task(id:) when
appropriate. Make work idempotent and cooperatively cancellable, and keep
CPU-heavy work off the main actor.
- Scope geometry observation and animation to the presentation they affect.
Avoid state-layout feedback loops and broad implicit animation.
- Give caches, decoded images, subscriptions, tasks, and persistence objects
explicit owners and bounded lifetimes. Verify release paths with memory
tools rather than inferring them from view disappearance.
Apply targeted optimizations
Prefer simpler fixes in this order:
- remove unused or overly broad dependencies;
- pass narrower render values;
- move repeated work out of
body and cache it correctly;
- isolate the affected subtree in a real child view;
- stabilize collection identity and row shape;
- reduce high-frequency event, geometry, or animation updates;
- adopt custom
Equatable behavior only when profiling still justifies it.
Apply items 1–6 immediately when source inspection proves the mechanism and the
change is low-risk and semantics-preserving. Do not leave such a correction as
advice merely because runtime tooling is unavailable. Keep custom equality,
container or cache-policy swaps, throttling, framework workarounds, and other
tradeoff-dependent changes measurement-gated.
When relying on custom equality:
- compare every value that affects content, layout, styling, accessibility,
identity, or action semantics;
- exclude a closure or handler only when its stability is an explicit,
reviewable invariant;
- ensure equality is cheaper than the skipped body work;
- use
.equatable() when the implementation relies on the custom equality
boundary, then verify the behavior on supported OS versions;
- remember that Airbnb's
@Equatable and @SkipEquatable are custom macros,
not SwiftUI APIs.
Diagnose runtime regressions instead of guessing
- Reproduce the symptom with the same workload.
- For responsiveness, profile a representative device and optimized build.
For ownership, use a diagnostic build when memory tooling requires it, then
confirm the user-facing workload in the optimized configuration.
- Separate:
- a long view-body or platform update;
- many individually short updates;
- main-thread or Core Animation commit work;
- render-server CPU or GPU work;
- memory or resource lifetime, and I/O, using separate evidence.
- Trace the most frequent or expensive cause to application code.
- Make one minimal correction and repeat the same capture.
- For a measurement-dependent change, reject it if the metric does not improve
reliably or correctness changes. For a direct source-proven correction, keep
it when focused checks preserve behavior and the stated mechanism is removed;
do not claim a measured gain.
Use Self._printChanges() only as temporary, best-effort debug evidence. It is
an underscored API with runtime cost; remove it before shipping.
Use Allocations, the Memory Graph Debugger, Leaks, and VM evidence for memory
symptoms. The SwiftUI instrument does not prove why an allocation remains live.
Guard against folklore
- Do not use a fresh
UUID(), mutable index, or non-unique \.self as identity.
- Do not assume
LazyVStack is always faster than VStack or List.
- Do not assume a lazy container immediately evicts off-screen state or
guarantees that row-local state survives a round trip.
- Do not assume
AsyncImage supplies the cache policy the product needs.
- Do not claim
resizable() down-samples decoded image memory.
- Do not use
@EnvironmentObject, @Binding, Group, AnyView, or
@ViewBuilder as a generic performance fix.
- Do not ban
AnyView, action closures, or weak self categorically; measure
the hot path and prove the ownership or identity problem.
- Do not ban
GeometryReader, onAppear, dismiss, or animations
categorically; constrain expensive effects and verify the case.
- Do not encode a framework workaround as a general rule without an OS and SDK
matrix, a minimal reproduction, and current profiling evidence.
- Do not describe SwiftUI's undocumented diffing internals as an API contract.
Verify completion
For changed code, require:
- formatting and static analysis used by the repository;
- a build for affected targets and platforms;
- focused tests for state, actions, identity, navigation, and cancellation;
- a review for accidental broad dependencies, unstable IDs, repeated work,
stale equality, and debug instrumentation.
For a performance claim, also report:
- device, OS, Xcode, build configuration, data volume, and interaction;
- before and after captures from the same scenario;
- whether the bottleneck was long work, frequent work, commit, or render;
- the metric improved and any remaining bottleneck.
For a memory claim, report the same repeated lifecycle checkpoints, peak and
post-interaction footprint or live-allocation counts, the proven retention
path, and whether the cache or resource policy reaches a bound.
Do not claim completion from fewer body logs alone.
When runtime evidence is unavailable, use “implemented from source evidence;
device profiling pending” and report the functional checks that passed.
1---2name: swiftui-optimization3description: Use when creating, refactoring, reviewing, or diagnosing SwiftUI views and screens where update frequency, body cost, state ownership, Observation, diffing, identity, lists, scrolling, memory lifetime, layout, animation smoothness, hangs, hitches, or Instruments evidence matter. Applies to new SwiftUI code, performance and memory audits, scrolling or animation regressions, and performance-focused code review across Apple platforms. Do not use for purely visual design work, non-SwiftUI rendering, or unsupported claims about undocumented SwiftUI internals.4---56# Build efficient SwiftUI views78## Outcome910Produce correct, maintainable SwiftUI views whose bodies finish quickly, update11only for relevant changes, and keep resources within their intended lifetimes.12Treat correctness as a gate. Use evidence proportionate to the change: direct13dependency, identity, work, control-flow, or ownership proof can justify a14low-risk refactor; use profiling to attribute a runtime regression or claim a15measured improvement.1617## Read references selectively1819- Read `references/data-flow-and-diffing.md` before choosing state ownership,20 passing model data, introducing `Equatable`, or reasoning about updates.21- Read `references/observation.md` before introducing `@Observable`, migrating22 from `ObservableObject`, or choosing `@State`, `@Bindable`, or environment23 injection for an observable model.24- Read `references/construction-patterns.md` before changing view boundaries,25 layout readers, tasks, presentation, or animations.26- Read `references/collections-and-scrolling.md` before changing `List`,27 `Table`, a lazy stack or grid, `ForEach`, programmatic scrolling, or a28 high-frequency feed.29- Read `references/memory-and-resources.md` before diagnosing memory growth,30 leaks, image footprint, cache retention, or persistence-backed screens.31- Read `references/profiling.md` before diagnosing an existing regression or32 claiming a performance improvement.33- Read `references/source-notes.md` when a recommendation is disputed,34 version-sensitive, or based on undocumented behavior.3536Repository instructions, supported deployment targets, and established37architecture override generic examples. They do not override the requirement38to preserve correctness and verify performance claims.3940## Establish the task41421. Read the repository's root and nearest local instructions.432. Inspect the deployment targets, Xcode and Swift versions, target platforms,44 existing state model, navigation architecture, and test commands.453. Classify the request as:46 - new view or screen;47 - behavior-preserving refactor;48 - measured performance diagnosis;49 - performance-focused code review.504. For a runtime-dependent issue, record one reproducible interaction,51 representative data volume, affected device and OS, build configuration, and52 visible symptom before making a performance claim. Do not require those53 runtime artifacts before correcting a direct source violation.545. Classify memory symptoms as transient growth, persistent growth, abandoned55 reachable memory, a reference-cycle leak, or an unbounded resource policy56 before changing ownership.576. Do not redesign unrelated architecture or add optimization machinery without58 a demonstrated need.5960Treat a request to create, optimize, fix, or refactor as edit authority within61its named scope. Do not downgrade it to code review because a profiler,62Simulator, physical device, or baseline capture is unavailable.6364## Use the SwiftUI mental model6566- Treat a `View` value as a description, not a persistent widget. SwiftUI may67 create view values and evaluate `body` frequently.68- Distinguish `body` evaluation, graph reconciliation, platform rendering, and69 display presentation. A logged `body` call is not proof of a rendered frame70 or a user-visible regression.71- Treat stored inputs, dynamic properties, environment values, and observable72 properties read by the view as dependencies.73- Preserve structural and explicit identity when the same semantic element74 should retain state and animate continuously.75- Create boundaries around coherent dependency sets, not arbitrary line counts.7677## Construct the view78791. Define the smallest render contract. Pass a child the values it displays80 instead of an entire feature state or broad store when practical.812. Choose each state wrapper by ownership and lifetime using82 `references/data-flow-and-diffing.md` and `references/observation.md`; never83 choose a wrapper because it is rumored to "render less."843. Keep `init` and `body` deterministic and cheap. Move I/O, decoding, large85 filtering or sorting, expensive formatting, and business logic into a model86 or service. Cache derived results with explicit invalidation.874. Extract a real `struct: View` when a subtree needs an independent update88 boundary. A computed `some View`, helper function, closure, `Group`, or89 `@ViewBuilder` helper does not create that boundary.905. Keep event sources and frequently changing dependencies in the smallest91 subtree that needs them.926. Use stable domain identity in `ForEach`, `List`, and `Table`. Follow93 `references/collections-and-scrolling.md` for row cardinality, lazy94 lifetime, prefetching, and high-frequency data.957. Tie asynchronous work to view lifetime with `.task` or `.task(id:)` when96 appropriate. Make work idempotent and cooperatively cancellable, and keep97 CPU-heavy work off the main actor.988. Scope geometry observation and animation to the presentation they affect.99 Avoid state-layout feedback loops and broad implicit animation.1009. Give caches, decoded images, subscriptions, tasks, and persistence objects101 explicit owners and bounded lifetimes. Verify release paths with memory102 tools rather than inferring them from view disappearance.103104## Apply targeted optimizations105106Prefer simpler fixes in this order:1071081. remove unused or overly broad dependencies;1092. pass narrower render values;1103. move repeated work out of `body` and cache it correctly;1114. isolate the affected subtree in a real child view;1125. stabilize collection identity and row shape;1136. reduce high-frequency event, geometry, or animation updates;1147. adopt custom `Equatable` behavior only when profiling still justifies it.115116Apply items 1–6 immediately when source inspection proves the mechanism and the117change is low-risk and semantics-preserving. Do not leave such a correction as118advice merely because runtime tooling is unavailable. Keep custom equality,119container or cache-policy swaps, throttling, framework workarounds, and other120tradeoff-dependent changes measurement-gated.121122When relying on custom equality:123124- compare every value that affects content, layout, styling, accessibility,125 identity, or action semantics;126- exclude a closure or handler only when its stability is an explicit,127 reviewable invariant;128- ensure equality is cheaper than the skipped body work;129- use `.equatable()` when the implementation relies on the custom equality130 boundary, then verify the behavior on supported OS versions;131- remember that Airbnb's `@Equatable` and `@SkipEquatable` are custom macros,132 not SwiftUI APIs.133134## Diagnose runtime regressions instead of guessing1351361. Reproduce the symptom with the same workload.1372. For responsiveness, profile a representative device and optimized build.138 For ownership, use a diagnostic build when memory tooling requires it, then139 confirm the user-facing workload in the optimized configuration.1403. Separate:141 - a long view-body or platform update;142 - many individually short updates;143 - main-thread or Core Animation commit work;144 - render-server CPU or GPU work;145 - memory or resource lifetime, and I/O, using separate evidence.1464. Trace the most frequent or expensive cause to application code.1475. Make one minimal correction and repeat the same capture.1486. For a measurement-dependent change, reject it if the metric does not improve149 reliably or correctness changes. For a direct source-proven correction, keep150 it when focused checks preserve behavior and the stated mechanism is removed;151 do not claim a measured gain.152153Use `Self._printChanges()` only as temporary, best-effort debug evidence. It is154an underscored API with runtime cost; remove it before shipping.155156Use Allocations, the Memory Graph Debugger, Leaks, and VM evidence for memory157symptoms. The SwiftUI instrument does not prove why an allocation remains live.158159## Guard against folklore160161- Do not use a fresh `UUID()`, mutable index, or non-unique `\.self` as identity.162- Do not assume `LazyVStack` is always faster than `VStack` or `List`.163- Do not assume a lazy container immediately evicts off-screen state or164 guarantees that row-local state survives a round trip.165- Do not assume `AsyncImage` supplies the cache policy the product needs.166- Do not claim `resizable()` down-samples decoded image memory.167- Do not use `@EnvironmentObject`, `@Binding`, `Group`, `AnyView`, or168 `@ViewBuilder` as a generic performance fix.169- Do not ban `AnyView`, action closures, or `weak self` categorically; measure170 the hot path and prove the ownership or identity problem.171- Do not ban `GeometryReader`, `onAppear`, `dismiss`, or animations172 categorically; constrain expensive effects and verify the case.173- Do not encode a framework workaround as a general rule without an OS and SDK174 matrix, a minimal reproduction, and current profiling evidence.175- Do not describe SwiftUI's undocumented diffing internals as an API contract.176177## Verify completion178179For changed code, require:180181- formatting and static analysis used by the repository;182- a build for affected targets and platforms;183- focused tests for state, actions, identity, navigation, and cancellation;184- a review for accidental broad dependencies, unstable IDs, repeated work,185 stale equality, and debug instrumentation.186187For a performance claim, also report:188189- device, OS, Xcode, build configuration, data volume, and interaction;190- before and after captures from the same scenario;191- whether the bottleneck was long work, frequent work, commit, or render;192- the metric improved and any remaining bottleneck.193194For a memory claim, report the same repeated lifecycle checkpoints, peak and195post-interaction footprint or live-allocation counts, the proven retention196path, and whether the cache or resource policy reaches a bound.197198Do not claim completion from fewer `body` logs alone.199When runtime evidence is unavailable, use “implemented from source evidence;200device profiling pending” and report the functional checks that passed.