← All topics/Performance & optimization

Senior / Staff iOS — interview prep

Performance & optimization

jank · Instruments · main thread · images · memory · launch · lists · concurrency · regressions

Time ProfilerMainActorDownsamplingNSCacheLaunch P95Stable IDsBounded tasks
01

Diagnosing scrolling jank

Q1
Fast interview flow
  • Reproduce on real device, Release, realistic data volume.
  • Start with Time Profiler + frame timelines to find where time is spent.
  • Separate root cause: layout, rendering, image decode, or main-thread work.
  • Fix one bottleneck at a time and re-profile.
Common high-impact fixes
  • Move decode/JSON/regex work off main.
  • Reduce expensive effects (blur, shadow, transparency layers).
  • Keep cells/rows cheap; avoid heavy work in hot render paths.
  • Use signposts to correlate spikes with your own code phases.
Signpost a critical path
import os.signpost

let log = OSLog(subsystem: "com.yourapp.feed", category: "perf")
os_signpost(.begin, log: log, name: "LoadPage")
defer { os_signpost(.end, log: log, name: "LoadPage") }
02

Instruments starter map

Q2
SymptomStart withLook for
High CPU / hangsTime ProfilerMain-thread hotspots, sync I/O, decode/parsing in hot paths
Memory keeps growingAllocations + Memory GraphNet growth by generation, retained object chains, cache policy
Leak suspicionLeaks + Memory GraphRetain cycles, unbalanced observer/delegate ownership
Scroll stutterCore Animation + Time ProfilerDropped frames, long commits, layout/render hotspots
03

Main-thread contention

Q3
Rule to memorize
  • Main thread = UI updates only.
  • Move network, decode, file/DB work off main.
  • Return to main only for final UI mutation.
Verification
  • Before/after profile with same scenario.
  • Main-thread stacks should get shorter and less frequent.
  • UI smoothness and touch latency should improve together.
Off-main pipeline, UI on main
func refresh() async throws {
    let dto = try await api.fetch()          // off-main work
    let viewState = await mapper.map(dto)    // off-main work
    await MainActor.run { apply(viewState) } // UI only
}
04

Image pipeline essentials

Q4
What to do
  • Downsample to display size before showing image.
  • Use memory cache for hot reuse and disk/URLCache for cold reuse.
  • Cancel in-flight loads on cell reuse and re-check item identity.
What hurts
  • Decoding full-res images for tiny thumbnails.
  • Unbounded caches that slowly push app toward OOM.
  • Applying stale async image results to reused rows.
Downsample decode pattern
let source = CGImageSourceCreateWithData(data as CFData, nil)!
let opts: [CFString: Any] = [
  kCGImageSourceCreateThumbnailFromImageAlways: true,
  kCGImageSourceThumbnailMaxPixelSize: 256,
  kCGImageSourceCreateThumbnailWithTransform: true
]
let cg = CGImageSourceCreateThumbnailAtIndex(source, 0, opts as CFDictionary)!
05

Memory growth and OOMs

Q5
i
Distinguish leaks (objects should die but stay alive) from high but valid usage (large datasets/images with weak limits).
Debug loop
  • Reproduce the same flow repeatedly with Allocations generations.
  • Take Memory Graph snapshots and inspect retain paths to root.
  • Confirm growth source, then add explicit cap/eviction policy.
Typical fixes
  • Cap caches and purge on memory warning.
  • Page large feeds instead of loading everything at once.
  • Break retain cycles in closures, delegates, and observers.
06

Launch time

Q6
Keep launch lean
  • Prioritize first interactive frame; defer non-critical work.
  • Avoid blocking network and heavy synchronous I/O at launch.
  • Audit expensive singleton/static initialization paths.
Measure correctly
  • Track P50/P95 launch on same OS/device class.
  • Use signposts for launch phases to localize regressions.
  • Change one thing, then compare same metric.
07

Table/collection performance

Q7
Core mechanics
  • Reuse must reset state and cancel async work in prepareForReuse.
  • Use prefetch APIs for upcoming rows and cancel when no longer needed.
  • Use stable IDs (same item keeps same unique id every reload) for diffable and SwiftUI lists.
Layout costs
  • Deep constraints and aggressive self-sizing can dominate scroll time.
  • Use good estimated sizes and keep row hierarchy simple.
  • Profile with Core Animation FPS and Time Profiler.
Estimated height baseline
tableView.estimatedRowHeight = 120
tableView.rowHeight = UITableView.automaticDimension
08

Concurrency strategy (memorize)

Q8
!
Memorize this line: UI on main, mutable state serialized, independent work parallel (bounded).
Practical rules
  • Shared mutable state (DB/cache writes) gets one writer (actor/serial queue).
  • Only parallelize independent CPU work, with a small cap.
  • Prefer structured concurrency; avoid detached task spray.
How to measure
  • Baseline on a real device in Release.
  • Change one thing.
  • Compare the same metric: time, FPS, CPU, and memory.
Bound parallelism
await withTaskGroup(of: Void.self) { group in
    for batch in batches.prefix(4) {
        group.addTask { await process(batch) }
    }
}
Performance & optimization — interview prep cheat sheetAligned with practical question set