This episode covers performance and memory optimization in Swift: ARC memory management with weak and unowned references, profiling with Instruments for memory, allocations, and the Time Profiler, code optimization with value types and lazy properties, plus strategies to reduce startup time and improve runtime performance.

A working app isn't necessarily a good-feeling app. A slow or memory-hungry app will be abandoned by users, whatever its features. Episode 14 covers performance and memory optimization in Swift: managing memory correctly with ARC, profiling with Instruments, and writing efficient code without sacrificing readability.
The main rule of this episode: measure first, optimize later. Optimization without data is speculation. Instruments gives you the facts about where time and memory actually go.
Episode 2 introduced ARC. Problems arise when two objects strongly reference each other — a retain cycle — so neither is ever freed:
class Pemilik {
var hewan: Hewan?
}
class Hewan {
weak var pemilik: Pemilik?
}
let pemilik = Pemilik()
let kucing = Hewan()
pemilik.hewan = kucing
kucing.pemilik = pemilikweak var pemilik: Pemilik? breaks one side of the cycle. A weak reference doesn't increase the retain count; when the pointed-to object is freed, the value automatically becomes nil. Use weak for parent-child relationships where neither owns the other.
When you're certain a reference always lives as long as the object holding it, use unowned:
class Dokumen {
unowned let pemilik: Pemilik
init(pemilik: Pemilik) {
self.pemilik = pemilik
}
}unowned let pemilik: Pemilik doesn't increase the retain count and isn't optional. The difference from weak: unowned never becomes nil, so accessing it after the object is freed is a crash. Use it only when the lifecycle is genuinely guaranteed.
A closure stored in a property can also create a retain cycle. Use a capture list:
class Pengolah {
var callback: (() -> Void)?
func siapkan() {
callback = { [weak self] in
self?.lakukan()
}
}
func lakukan() {
print("Diproses")
}
}[weak self] in the capture list prevents the closure from strongly holding self. This pattern is mandatory for long-lived closures — such as notification handlers or network callbacks.
Instruments is Xcode's built-in profiling tool. The templates you'll use most:
From the terminal, Instruments can be run for automation:
xcrun xctrace record --template "Time Profiler" \
--launch ./Aplikasi --output profil.tracexcrun xctrace record --template "Time Profiler" records a CPU profile of the app and saves it as a trace. In Xcode, choose a template and press record — real interaction with the app during recording produces the most realistic data.
The questions you answer from the data: which functions take the longest? How many objects are allocated per second? Are there objects that are never released? Focus on the genuinely expensive areas — don't optimize code that only runs once.
Data structures that change frequently should use value types. Swift applies copy-on-write to collections: an actual copy only happens when the value is mutated:
var daftar1 = [Int](repeating: 1, count: 1_000_000)
var daftar2 = daftar1
daftar2[0] = 99var daftar2 = daftar1 doesn't immediately copy a million elements — both share the buffer until daftar2[0] = 99 triggers the copy. Value types also make code more predictable for concurrency.
A lazy property is initialized only on first access, saving work when the object is created:
struct Analisa {
lazy var hasilBerat = {
(1...100_000).reduce(0, +)
}()
}lazy var hasilBerat = { ... }() defers an expensive computation until it's actually needed. Great for values that are rarely accessed or depend on state after object initialization.
Startup time is the user's first experience. Strategies to reduce it:
application(_:didFinishLaunchingWithOptions:) — move heavy initialization later or to idle time.lazy and dependency injection to defer object creation.At runtime, watch for patterns that commonly slow things down:
var teks = [String]()
teks.reserveCapacity(10_000)
for i in 0..<10_000 {
teks.append("Baris \(i)")
}
let gabungan = teks.joined(separator: "\n")teks.reserveCapacity(10_000) allocates the buffer once to avoid repeated doubling during appends. Understanding these allocation patterns makes your code far more efficient without changing behavior.
Info
Measure the impact of every optimization before and after with Instruments. If there's no measurable difference, the optimization probably adds complexity without benefit — drop it and focus on real problems.
Key takeaways:
weak breaks retain cycles; unowned is used when the lifecycle is guaranteed.[weak self] capture list prevents closures from strongly holding objects.lazy properties defer expensive computations until needed.In the next episode, episode 15, we'll cover testing and quality assurance — unit testing with XCTest, UI testing and snapshot testing, mocking dependencies with test doubles, and continuous integration for Swift projects. Your quality becomes guaranteed automatically!