When do Swift globals and static properties initialise, and how does that move work into or out of launch?

21.11.1 · L3 · Interview

Predict first

In what order do a main.swift global, a static let and a global in another file initialise?

import Foundation

func run(_ exe: String, _ args: [String]) -> String {
    let p = Process(), out = Pipe()
    p.executableURL = URL(fileURLWithPath: exe); p.arguments = args; p.standardOutput = out
    try! p.run(); p.waitUntilExit()
    return String(decoding: out.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
}
let dir = NSTemporaryDirectory() + "lazyinit-\(getpid())"
try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
try! """
func make(_ name: String) -> Int { print("init", name); return 1 }
var inMain = make("global in main.swift")
print("main body starts")
print(Services.shared, otherFileGlobal)
enum Services { static let shared = make("static let") }
""".write(toFile: dir + "/main.swift", atomically: true, encoding: .utf8)
try! """
let otherFileGlobal = { () -> Int in print("init global in another file"); return 2 }()
""".write(toFile: dir + "/other.swift", atomically: true, encoding: .utf8)
_ = run("/usr/bin/swiftc", ["-swift-version", "5", dir + "/main.swift", dir + "/other.swift", "-o", dir + "/app"])
print(run(dir + "/app", []), terminator: "")
try? FileManager.default.removeItem(atPath: dir)
  • main.swift global first, then the main body, then the static let and the other-file global on first use
  • All initialise before main starts
  • The static let initialises first

Mental model. Preparing every dish for a banquet before the doors open (eager) versus cooking each dish when the first guest orders it (lazy). The first order is slower, and if that guest arrives at opening time the kitchen is slow at opening time.

The rules differ by where the declaration is. A global in main.swift (top-level code) runs eagerly, in order, as part of the script. A global or static stored property in any other file is lazy: it is initialised the first time it is accessed, thread-safely, through a one-time initialiser (swift_once). The check compiles two files with swiftc and prints the order: global in main.swift (before anything else), main body starts, then static let and global in another file when the line that reads them executes. In an app there is normally no top-level code (the @main attribute or UIApplicationMain generates the entry point), so your static let shared = … singletons and app-level globals are all lazy: they run at the first use, not at launch.

Why launch cares: a heavy singleton (a database stack, an analytics SDK, a large dictionary) that is first touched in didFinishLaunching or the first view’s body runs on the main thread before the first frame, and its cost lands exactly there. Moving the first access later (on demand, after the first frame, or on a background queue) lowers launch time without changing the code; making the initialiser expensive and reading it during launch does the opposite. Do not rely on lazy order for correctness: initialisers that depend on each other and on side effects get fragile, and an initialiser that touches the main actor from the wrong thread is a bug. Use signposts around the first access to attribute the cost (Q430). Not asserted: Objective-C +load and C static constructors, which do run before main and were not part of this check.

Top-level main.swift variables are eager; other globals and statics are lazy.

Common traps

  • Assuming globals all initialise at launch. Outside main.swift they are lazy.
  • Putting expensive work in a singleton that the first screen reads. It lands on the launch path.

They might follow up with

  • Is lazy initialisation thread-safe? Globals and statics are; a lazy var on an instance is not.
  • What runs before main? dyld loading, and static initialisers in your linked code, which you reduce by linking fewer frameworks (Q429).

Related questions

  • 21.11.2 How do you measure launch time from inside the app, and what can make the number wrong?
  • 21.1.1 How do you approach a performance problem, and which kinds of problem are there?
  • 21.2.2 How do signposts and Points of Interest work, and how do you turn a measurement into an XCTest performance…
  • 21.2.1 Which Instruments templates do you reach for, how do you read a Time Profiler trace, and where can a sampling…

How do you measure launch time from inside the app, and what can make… →