Performance and Instruments
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
"It feels slow" is not a bug report you can fix by reading code. Performance work starts with a measurement, finds the one place that dominates it, changes that place, and measures again. Instruments, the profiler that ships with Xcode, is the tool for the middle two steps. This page applies it to FieldNotes with the 2026 baseline of this section: iOS, iPadOS, macOS, watchOS and visionOS 27, Xcode 27 and Swift 6.4, with a deployment minimum of 26. It follows one method through CPU time, responsiveness (hangs, hitches, SwiftUI updates and Swift concurrency), blocking, memory, energy, launch time and app size, and uses the Instruments 27 additions: the Top Functions mode, Run Comparisons and the Swift Executors instrument.
Field data (the Organizer’s metrics and MetricKit reports) tells you which of these areas to work on; it is set
up on Logging, MetricKit, Crash Reports and Organizer,
together with the FieldNotesSignposts.loading signposter this page records. Correctness bugs and leaks found in
the debugger are on Debugging with Xcode and LLDB, and
automated performance tests (measure(metrics:)) are on
UI Tests, Test Plans, Performance Tests and Coverage.
The Method: Measure, Identify, Fix, Verify
Every performance fix on this page goes around the same loop. Measure the symptom on a real device, in a Release build, around a reproducible interaction. Identify the single biggest cause in the trace. Fix one thing. Verify by recording the same interaction again and comparing, then either stop or go around again.
Figure: The performance loop
Release build on a device,
Product > Profile (Command-I)"] --> I["Identify
the biggest cause:
track, call tree, top function"] I --> F["Fix
one change:
less work, off the main actor, no blocking"] F --> V["Verify
record again,
Run Comparison on the same signpost"] V -- "still over budget" --> M V -- "within budget" --> Done(["Ship, and watch the
Organizer and MetricKit"]) Field["Organizer, MetricKit,
tester reports"] --> M
Three rules keep the loop honest:
-
Profile Release builds. Product > Profile (Command-I) builds with the Release configuration by default and opens Instruments. A Debug build is unoptimized and full of checks, so its hot spots are often not the real ones.
-
Profile on a device. The Simulator runs on the Mac’s CPU and memory and has no watchdog, thermal limits or real GPU. Use the slowest device you support.
-
Mark the interaction. Wrap the interaction in a signpost interval so every trace can be filtered to exactly that time range. FieldNotes'
FieldNotesSignposts.loadingintervals appear in the Points of Interest track.
Instruments can also record from the command line with xctrace, which is how a CI job or a script captures the
same trace repeatedly:
xcrun xctrace list templates
xcrun xctrace list devices
# Record 30 seconds of the Time Profiler template while launching FieldNotes on a paired iPhone
xcrun xctrace record --template 'Time Profiler' --device 'Field iPhone' \
--time-limit 30s --output FieldNotes-search.trace --launch -- com.example.fieldnotes
# Export the table of contents of a trace, restricted to its first 10 seconds (xctrace 27)
xcrun xctrace export --input FieldNotes-search.trace --toc --time-end 10s
xctrace in Xcode 27 also accepts recording options as JSON (--show-recording-options,
--recording-options) and can export a time range directly. Instruments 27 needs a target device with at least
iOS 17 or watchOS 10.
CPU: Time Profiler
The Time Profiler samples the call stack of every thread about once a millisecond. A function’s weight is the number of samples in which it appeared; its self weight counts only samples where it was the function actually running. Instruments 27 shows the samples three ways, switched with the control above the detail view:
-
Call Tree: the classic outline, from
maindown to the leaves. Use Invert Call Tree and Hide System Libraries to bring your own heaviest functions to the top. -
Flame Graph: the same tree as nested bars, callers on top, width proportional to time. It shows expensive paths at a glance, but spreads the cost of a function called from many places across many small bars.
-
Top Functions (new in 27): discards the hierarchy and merges every occurrence of a function into one entry, sorted by self weight, with a flame graph of its callers next to it. It finds overhead that is scattered, such as Swift runtime functions, retain and release, or existential unboxing.
FieldNotes' search field got slow with a few thousand notes. The first version filtered in the view’s body, so
every keystroke re-ran a case- and diacritic-insensitive comparison of every note, on the main actor:
// FieldNotes/Views/NoteSearchResults.swift (first version: filters in body)
import SwiftUI
import FieldNotesModel
import FieldNotesServices
struct NoteSearchResults: View {
@Environment(NoteStore.self) private var store
let query: String
var body: some View {
let signposter = FieldNotesSignposts.loading
let results = signposter.withIntervalSignpost("Search") {
store.notes.filter {
$0.title.localizedStandardContains(query) || $0.body.localizedStandardContains(query)
|| $0.tags.contains { $0.localizedStandardContains(query) }
}
}
List(results) { NoteRow(note: $0) }
}
}
Filtered to the Search signpost, the main thread is at 100% CPU and Top Functions lists string comparison and Unicode normalization at the top. The fix does the expensive part once per note instead of once per keystroke, and moves each search off the main actor. The views then only show a ready array:
// FieldNotes/Search/NoteSearchMatcher.swift (page-local; the word index on Swift Concurrency in Apps is a different type)
import Foundation
import FieldNotesModel
/// Folded, lowercased text per note, built once and searched off the main actor.
nonisolated struct NoteSearchMatcher: Sendable {
private let entries: [(id: Note.ID, text: String)]
init(notes: [Note]) {
entries = notes.map { note in
let text = ([note.title, note.body] + note.tags).joined(separator: " ")
return (note.id, text.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: nil))
}
}
@concurrent
func matches(_ query: String) async -> Set<Note.ID> {
let folded = query.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: nil)
return Set(entries.lazy.filter { $0.text.contains(folded) }.map(\.id))
}
}
// FieldNotes/Views/NoteSearchResults.swift (fixed)
struct NoteSearchResults: View {
/// What the results depend on: the query and the notes' contents. `Note` is `Hashable`.
private struct SearchInput: Hashable {
var query: String
var notes: [Note]
}
@Environment(NoteStore.self) private var store
@State private var matchingIDs: Set<Note.ID> = []
let query: String
var body: some View {
List(store.notes.filter { matchingIDs.contains($0.id) }) { NoteRow(note: $0) }
.task(id: SearchInput(query: query, notes: store.notes), name: "Search") {
let index = NoteSearchMatcher(notes: store.notes) // cheap enough; cache it for larger stores
matchingIDs = await index.matches(query)
}
}
}
#Preview {
NoteSearchResults(query: "beetle")
.environment(NoteStore(notes: [Note(title: "Beetle traps, site A", tags: ["beetles"])]))
}
.task(id:) cancels the previous search and starts a new one whenever its ID changes. Keying it on SearchInput
rather than on query alone means an edited, added or deleted note refreshes the results too; comparing the array
is a linear pass over values already in memory, far cheaper than the folding and matching it guards. Task identity
and cancellation are explained on Swift Concurrency in Apps; @concurrent
runs the matching on the global concurrent executor. The filter left in body is a cheap Set lookup.
Verifying with Run Comparisons (27)
To verify, record the same search again into the same trace document, filter both runs to the same Search signpost interval, select the main thread track, and click Compare in the middle bar to choose the baseline run. Instruments 27 matches each function in the baseline with its counterpart in the new run and shows the difference: red for regressions, green for improvements, in the call tree, the flame graph or Top Functions. Comparisons are saved in the trace document, so a teammate can review the evidence. Recording both runs from the command line keeps the conditions identical:
# Baseline, then the fixed build, recorded the same way
xcrun xctrace record --template 'Time Profiler' --device 'Field iPhone' --time-limit 30s \
--output Search-baseline.trace --launch -- com.example.fieldnotes
xcrun xctrace record --template 'Time Profiler' --device 'Field iPhone' --time-limit 30s \
--output Search-fixed.trace --launch -- com.example.fieldnotes
# Drag both onto the Instruments sidebar (27) to get both runs in one document, then Compare.
Expect a comparison to show some red: a fix often introduces new, cheaper functions, which appear as regressions. Judge the total of the interval, not individual rows.
Responsiveness: Hangs and Hitches
Hangs
A hang is a period in which the main run loop is too busy to handle input. Apple’s tools start reporting when the main run loop is unresponsive for more than 250 ms; below about 100 ms a delay after a tap is rarely noticeable, and a few hundred milliseconds already feels broken. The Hangs instrument, included in the Time Profiler and Swift Concurrency templates, marks each hang on the timeline and can use a lower threshold. The Organizer and MetricKit report the hang rate from the field.
Hitches
A hitch is a frame that appears later than expected during an animation or scroll. Each frame has a deadline: the app must commit its view changes, and the render server must render them, before the next display refresh. A commit hitch means the app’s main thread was late (the same long main-thread work that causes hangs); a render hitch means the render server was late, usually because the frame was too expensive to draw: many offscreen passes, large blurs, too many Liquid Glass effects at once, or a shader compiled on first use. The Animation Hitches instrument records each frame’s lifetime and hitch time; in Instruments 27 it also works on Apple Vision Pro. The Organizer’s new Hitches metric covers all animations, not only scrolling.
# Scroll the notes list for 20 seconds while recording frame lifetimes
xcrun xctrace record --template 'Animation Hitches' --device 'Field iPhone' \
--time-limit 20s --output FieldNotes-scroll.trace --attach FieldNotes
See Apple Developer Documentation — Understanding hitches in your app and Apple Developer Documentation — Understanding hangs in your app.
Specific causes are covered where the feature is: shaders can be precompiled with Shader.compile(as:)
(Drawing, Animation and Gestures), list rows should
be cheap and have a stable identity (Lists, Tables and
Collections), and the number of glass effects on screen should stay small
(Liquid Glass, Materials and Icons).
Figure: High CPU or a blocked main thread?
When the app hangs or hitches, the first question is what the main thread is doing during that time. The answer picks the instrument and the kind of fix:
during the hang?"} Q1 -- "high, near 100%" --> Q2{"Is the work needed
on the main actor?"} Q2 -- "no" --> OFF["Move it off the main actor:
@concurrent, a model actor,
Swift Executors instrument to confirm"] Q2 -- "yes, it is UI work" --> OPT["Do less: Time Profiler, Top Functions,
SwiftUI instrument for long body updates"] Q1 -- "low, mostly idle" --> Q3{"What is it waiting for?
System Trace"} Q3 -- "file I/O, a system call" --> IO["Do the I/O off the main actor"] Q3 -- "a lock or semaphore" --> LOCK["Remove the synchronous wait:
await instead of blocking"] Q3 -- "another process (IPC)" --> IPC["Call the async API,
or call it earlier"] Q3 -- "runnable but not running" --> PRIO["Too many threads competing:
check QoS and thread priority"]
The WWDC26 session Profile, fix, and verify walks through this flow with a note-taking app very close to FieldNotes: a CPU-bound hang fixed with Top Functions, a hang caused by thumbnail tasks on the main actor, and a hang caused by a synchronous file write.
The SwiftUI Instrument
The SwiftUI template records every update SwiftUI performs next to a Time Profiler track. Its lanes show View Body Updates, Platform View Updates and Other Updates per module, and long updates are colored so they stand out. Two kinds of problem show up:
-
Long updates: a single
bodythat takes too long. Select it, set the inspection range, and use the Time Profiler track (or its flame graph) to see what the body runs. -
Too many updates: each short, but so many that SwiftUI is busy for a long time. Choose Show Causes on an update to open the cause-and-effect graph: nodes are your views, observable properties, environment values and transactions (blue for your code, gray for the system), and edges say which change caused which update.
Xcode 27 adds a Summary of Updates focus action on the view hierarchy detail and records layout passes with the
reason a layout wasn’t cached. A typical FieldNotes finding was a row that created a formatter in body and
redrew whenever any note changed, because it read the whole store.notes array:
// Before: runs a DateFormatter per row per update, and depends on every note in the store.
struct PinnedBadgeRow: View {
@Environment(NoteStore.self) private var store
let note: Note
var body: some View {
let formatter = DateFormatter()
formatter.dateStyle = .medium
let pinnedCount = store.notes.filter(\.isPinned).count
return Text("\(note.title), \(formatter.string(from: note.createdAt)) (\(pinnedCount) pinned)")
}
}
// After: the row depends only on its own note; the count is read once by the list header.
struct PinnedBadgeRowFixed: View {
let note: Note
var body: some View {
Text("\(note.title), \(note.createdAt, format: .dateTime.day().month().year())")
}
}
#Preview {
List { PinnedBadgeRowFixed(note: Note(title: "Beetle traps, site A")) }
}
With @Observable, a view depends exactly on the properties its body reads, so the cure for too many updates
is usually to read less: pass a value instead of the store, move derived values into the model, and avoid storing
closures that capture the parent view. The WWDC25 session Optimize SwiftUI performance with Instruments works
through both problem kinds.
Swift Concurrency and the Swift Executors Instrument (27)
The Swift Concurrency template records tasks and actors: when each task is created, running, suspended or
waiting, and how long work waits in an actor’s queue. Task names from Task(name:) and .task(name:) label the
tracks (see Swift Concurrency in Apps); in Instruments 27, tasks are
grouped into Swift Task Collection tracks by name or place of creation, and selecting an interval of an actor’s
queue lists the tasks waiting in it. Recorded together with Time Profiler, tasks, actors and executors gain a
Profile detail, a call tree of the samples taken while they were running. Instruments 27 also adds the Swift
Executors instrument, with one track for the main actor, one for the cooperative thread pool and one for each
custom SerialExecutor or TaskExecutor, so you see where each task’s work ran.
That is how FieldNotes found why scrolling a notebook of photo notes hitched. The previews were rendered in a
view’s .task, which inherits the main actor, so every render competed with scrolling on the main actor track:
// FieldNotes/Views/NotePreviewImage.swift
import SwiftUI
import FieldNotesModel
nonisolated struct NotePreviewRenderer: Sendable {
// Before: a synchronous function, called from a main-actor .task, so it ran on the main actor.
// func render(_ note: Note, size: CGSize) -> CGImage? { ... }
// After: @concurrent always runs on the global concurrent executor, and renders in parallel.
@concurrent
func render(_ note: Note, size: CGSize) async -> CGImage? {
guard !Task.isCancelled else { return nil }
return drawPreview(of: note, size: size) // Core Graphics drawing, several ms per note
}
private func drawPreview(of note: Note, size: CGSize) -> CGImage? { /* ... */ nil }
}
struct NotePreviewImage: View {
let note: Note
@State private var image: CGImage?
var body: some View {
Group {
if let image { Image(decorative: image, scale: 2) } else { Color.secondary.opacity(0.1) }
}
.frame(width: 60, height: 60)
.task(id: note.id, name: "Render preview") {
image = await NotePreviewRenderer().render(note, size: CGSize(width: 120, height: 120))
}
}
}
In the new trace the Render preview tasks move from the main actor track to the cooperative pool track of the Swift Executors instrument, and the hitches disappear. Secondary sources also describe Swift actor data inside Time Profiler and Allocations in Instruments 27; the Xcode 27 release notes confirm the Time Profiler side (the Profile detail above) but say nothing about Allocations, so this page doesn’t rely on it.
A Blocked Main Thread: System Trace
When a hang shows low CPU on the main thread, the thread isn’t slow, it is waiting. Time Profiler only samples running code, so it can’t see this. The System Trace template records thread states (running, runnable, blocked), system calls, virtual memory faults and scheduling. Instruments 27 draws system calls, VM faults and thread states in one plot, follows the chain of events that made a thread runnable, graphs thread priority over time, and shows each system call’s arguments in the inspector.
FieldNotes' Back Up Notebook command froze the UI for half a second. System Trace showed the main thread
blocked inside a single write system call of a large file:
// FieldNotes/Export/NotebookBackup.swift
import Foundation
import FieldNotesModel
nonisolated struct NotebookBackup: Sendable {
let destination: URL
// Before: called from a Button action on the main actor.
// func write(_ notes: [Note]) throws {
// try PropertyListEncoder().encode(notes.map(BackupRecord.init)).write(to: destination, options: .atomic)
// }
// After: encoding and the blocking write run off the main actor.
@concurrent
func write(_ notes: [Note]) async throws {
let data = try PropertyListEncoder().encode(notes.map(BackupRecord.init))
try Task.checkCancellation()
try data.write(to: destination, options: .atomic)
}
}
/// The on-disk shape of one note in a backup.
nonisolated struct BackupRecord: Codable, Sendable {
let id: UUID, title: String, body: String, createdAt: Date, tags: [String]
init(_ note: Note) {
id = note.id; title = note.title; body = note.body; createdAt = note.createdAt; tags = note.tags
}
}
// The command (main actor):
// Button("Back Up Notebook") {
// Task(name: "Back up notebook") {
// do { try await NotebookBackup(destination: url).write(notes) }
// catch { backupError = error } // shown in an alert, never dropped
// }
// }
xcrun xctrace record --template 'System Trace' --device 'Field iPhone' \
--time-limit 15s --output FieldNotes-backup.trace --attach FieldNotes
After the fix, the write call appears on a pool thread and the main thread stays runnable. The same template
finds waits on locks, semaphores and synchronous IPC; the fix is always to stop waiting synchronously on the main
actor, not to make the waited-for work faster.
Memory: Allocations and Leaks
The Allocations instrument records every heap and virtual memory allocation with its size, category and
allocating backtrace. Its most useful feature for an app is generations: click Mark Generation before an
interaction, perform it, return to the starting screen and mark again. Everything that survives in the new
generation was allocated during the interaction and never freed. For FieldNotes, opening and closing the note
composer should leave nothing behind; the first version of DraftAutosaver left one autosaver per cycle, the
retain cycle traced on Debugging with Xcode and LLDB. The
Leaks instrument checks the heap periodically for unreachable objects and shows each leaked cycle as a graph,
which is why this generations-and-Leaks workflow has superseded hand-driven leak hunting with breakpoints and
ad-hoc heap scripts in LLDB; malloc stack logging with leaks, heap and vmmap (see
Debugging with Xcode and LLDB) remains the command-line
complement to it.
# Record Allocations and Leaks together while opening and closing the composer
xcrun xctrace record --template 'Leaks' --device 'Field iPhone' \
--time-limit 60s --output FieldNotes-composer.trace --launch -- com.example.fieldnotes
Leaks only finds memory nothing references. Memory that is still referenced but no longer needed (an
ever-growing cache, the whole store’s photos decoded at full size) is abandoned memory, and only generations
show it. Put regenerable data in URL.cachesDirectory or an NSCache, decode images at display size, and respond
to memory warnings. Extensions, especially widgets, have much lower memory limits than the app; MetricKit’s new
memory exception diagnostic reports when one was terminated for it.
Energy: Power Profiler
The Power Profiler instrument shows the system’s power use and, for the app, its power impact per subsystem: CPU, GPU, display and networking, plus the thermal state and charging status. It needs an iPhone or iPad running 26 or later. Because a cable charges the device (and Xcode keeps it awake), the most realistic traces come from the device itself: turn on Developer Mode, enable Settings > Developer > Performance Trace in Power Profiler mode for FieldNotes, start and stop the trace from Control Center, and open the file in Instruments later. From a paired device over Wi-Fi:
xcrun xctrace record --instrument 'Power Profiler' --device 'Field iPhone' \
--time-limit 10m --output FieldNotes-fieldtrip.trace --attach FieldNotes
For FieldNotes the expensive scenario is a field trip: location updates, photos and background sync for hours. Compare traces of the same scenario before and after a change, on the same device model; power impact values aren’t comparable across models. The usual savings are fewer, batched network requests (see Networking: URLSession, Network Framework and ATS), coarser location accuracy when precision isn’t needed, and letting the system schedule deferrable work (Background Execution).
Launch Time
Launch time is measured from the tap on the icon to the first frame. A cold launch (after a reboot, or after the system evicted the app and its dependencies) is much slower than a warm one, so measure several kinds. The App Launch template records a time profile and thread states for the launch, split into phases (process initialization, system interface initialization, initial scene rendering, first frame):
xcrun xctrace record --template 'App Launch' --device 'Field iPhone' \
--output FieldNotes-launch.trace --launch -- com.example.fieldnotes
Before the first frame, FieldNotes' AppServices opens the SwiftData store, and every scene injects the store.
That is required; loading all notes is not. FieldNotes shows its window immediately and loads the notes in the
first scene’s .task with loadNotesAtLaunch(_:), which the Points of Interest track shows as Load notes
and MetricKit reports as an extended launch task (both defined on
Logging, MetricKit, Crash Reports and Organizer):
// FieldNotes/ContentView.swift (excerpt, updated: load after the first frame)
NavigationSplitView { /* ... */ }
.task(name: "Launch load") { await loadNotesAtLaunch(store) }
Other launch costs to look for: many dynamic frameworks (consider static linking or mergeable libraries, see Swift Packages and Modularizing an App), static initializers in Objective-C or C++ dependencies (the dyld Activity instrument measures them), and synchronous work in the app delegate. If launch takes too long, the system’s watchdog terminates the app, which shows up as a crash report with a watchdog termination reason.
App Size and Thinning
Download and install size matter on cellular connections and full devices. The App Store thins the app: each
device downloads only its own architecture and asset variants. Build products (the .app, the .xcarchive, the
uploaded .ipa) are not representative sizes. To measure, export the archive for development or ad hoc
distribution with thinning for all variants; the export folder then contains App Thinning Size Report.txt with
compressed (download) and uncompressed (install) sizes per variant:
<?xml version="1.0" encoding="UTF-8"?>
<!-- SizeReportExportOptions.plist: a development export used only for the size report -->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>development</string>
<key>teamID</key>
<string>ABCDE12345</string>
<key>thinning</key>
<string><thin-for-all-variants></string>
</dict>
</plist>
xcodebuild -exportArchive -archivePath build/FieldNotes.xcarchive \
-exportPath build/SizeReport -exportOptionsPlist SizeReportExportOptions.plist
head -20 "build/SizeReport/App Thinning Size Report.txt"
The angle brackets of <thin-for-all-variants> must be escaped in the plist, as shown. The App Store export uses
the canonical ExportOptions.plist from
App Store Connect, TestFlight and App Review; App
Store Connect then shows the real size of each variant. Typical savings are asset catalogs instead of loose images
(so they can be thinned), removing unused resources, compressing large media, and moving rarely used content to
Background Assets. The Organizer’s new Storage metrics in Xcode 27 track app size and Documents & Data across
releases.
Specialized Instruments
A few templates belong to a single framework and are covered with it:
-
Foundation Models: prompts, tool calls, token counts and latency of on-device model requests; see Apple Intelligence and Machine Learning.
-
RealityKit Trace: frame time, entity counts and rendering cost of RealityKit scenes such as FieldNotes' notes map; profile on the device, since the Simulator doesn’t reflect GPU cost:
xcrun xctrace record --template 'RealityKit Trace' --device 'Field Vision Pro' \
--time-limit 30s --output FieldNotes-map.trace --attach FieldNotes
Availability and Requirements
Platform availability
| API or feature | iOS | iPadOS | macOS | watchOS | visionOS |
|---|---|---|---|---|---|
Time Profiler, call tree and flame graph |
Yes |
Yes |
Yes |
Yes |
Yes |
Top Functions and Run Comparisons (Instruments 27) |
New in 27 |
New in 27 |
New in 27 |
New in 27 |
New in 27 |
Hangs instrument |
Yes |
Yes |
Yes |
Yes |
Yes |
Animation Hitches instrument |
Yes |
Yes |
Yes |
Not documented |
New in 27 (device) |
SwiftUI instrument |
Yes |
Yes |
Yes |
Yes |
Yes |
Swift Executors instrument (executor names) |
New in 27 |
New in 27 |
New in 27 |
New in 27 |
New in 27 |
System Trace, Allocations, Leaks |
Yes |
Yes |
Yes |
Yes |
Yes |
Power Profiler |
Yes (26 or later) |
Yes (26 or later) |
Not available |
Not available |
Not available |
App Launch template |
Yes |
Yes |
Yes |
Not documented |
Not documented |
App Thinning Size Report |
Yes |
Yes |
Not applicable |
Yes |
Yes |
Requirements
-
Profiling on a physical device needs Developer Mode and a signing team; Instruments 27 supports devices with iOS 17, watchOS 10, tvOS 17 or later.
-
Power Profiler traces need an iPhone or iPad with 26 or later; on-device Performance Trace needs Developer Mode.
-
The Swift Executors instrument names executors only on 27 systems; older systems show "Unknown executor".
-
The Animation Hitches instrument on visionOS needs a device running visionOS 27.
Where to Go Next
Guard the gains with automated measurements: XCTClockMetric and XCTApplicationLaunchMetric performance tests
on UI Tests, Test Plans, Performance Tests and Coverage,
run on every change by the pipeline on CI/CD with Xcode
Cloud and the Command Line. Watch the field results in the Organizer and MetricKit, set up on
Logging, MetricKit, Crash Reports and Organizer.
References
-
Apple Developer Documentation — Improving your app’s performance
-
Apple Developer Documentation — Profiling apps using Instruments
-
Apple Developer Documentation — Improving app responsiveness
-
Apple Developer Documentation — Understanding hangs in your app
-
Apple Developer Documentation — Understanding hitches in your app
-
Apple Developer Documentation — Understanding and improving SwiftUI performance
-
Apple Developer Documentation — Gathering information about memory use
-
Apple Developer Documentation — Reducing your app’s memory use
-
Apple Developer Documentation — Measuring your app’s power use with Power Profiler
-
Apple Developer Documentation — Reducing your app’s launch time
-
Apple Developer Documentation — Doing basic optimization to reduce your app’s size
-
Apple Developer Documentation -- withIntervalSignpost(_:id:around:)
-
Apple Developer Documentation -- task(id:name:priority:file:line:_:)
-
WWDC26 — Profile, fix, and verify: Improve app responsiveness with Instruments