Learn Swift - Performance & Memory Optimization
Series/Learn Swift/Episode 14
Episode 14 of 23

Learn Swift - Performance & Memory Optimization

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.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

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.

ARC and Weak References

Retain Cycles and weak

Episode 2 introduced ARC. Problems arise when two objects strongly reference each other — a retain cycle — so neither is ever freed:

Retain cycle and its solution
class Pemilik {
    var hewan: Hewan?
}
 
class Hewan {
    weak var pemilik: Pemilik?
}
 
let pemilik = Pemilik()
let kucing = Hewan()
pemilik.hewan = kucing
kucing.pemilik = pemilik

weak 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.

unowned for Non-Optional References

When you're certain a reference always lives as long as the object holding it, use unowned:

unowned reference
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.

Closures and Capture Lists

A closure stored in a property can also create a retain cycle. Use a capture list:

Capture list in a closure
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.

Profiling with Instruments

Opening Instruments

Instruments is Xcode's built-in profiling tool. The templates you'll use most:

  • Time Profiler: finds the functions consuming the most CPU.
  • Allocations: tracks object allocations and reveals memory leaks.
  • Memory Leaks: detects objects that are never freed.
  • Network: observes requests and network latency.

From the terminal, Instruments can be run for automation:

Run profiling from the CLI
xcrun xctrace record --template "Time Profiler" \
  --launch ./Aplikasi --output profil.trace

xcrun 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.

Reading Profiling Results

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.

Optimizing Swift Code

Value Types and Copy-on-Write

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:

Measuring copy-on-write
var daftar1 = [Int](repeating: 1, count: 1_000_000)
var daftar2 = daftar1
daftar2[0] = 99

var 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.

Lazy Properties

A lazy property is initialized only on first access, saving work when the object is created:

Lazy property
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.

Reducing Startup and Improving Runtime

Startup Time

Startup time is the user's first experience. Strategies to reduce it:

  • Reduce work in application(_:didFinishLaunchingWithOptions:) — move heavy initialization later or to idle time.
  • Use lazy and dependency injection to defer object creation.
  • Avoid loading files and decoding large JSON at launch.
  • Limit work in global and static initializers.

Runtime Performance

At runtime, watch for patterns that commonly slow things down:

  • Repeated measurements inside a loop — hoist calculations out.
  • Allocation in hot paths — reuse existing buffers.
  • String interpolation in large loops — accumulate in an efficient form, then join.
Avoiding allocation in a loop
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.

Closing

Key takeaways:

  • weak breaks retain cycles; unowned is used when the lifecycle is guaranteed.
  • The [weak self] capture list prevents closures from strongly holding objects.
  • Instruments provides Time Profiler, Allocations, and Leaks for factual data.
  • Value types with copy-on-write keep code efficient and safe for concurrency.
  • lazy properties defer expensive computations until needed.
  • Optimize based on data; measure before and after each change.

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!

Learn Swift - Performance & Memory Optimization | Learn Swift