Swift Lazy Properties: Deferred Initialization and Performance
Swift lazy properties let you postpone creating a stored property until the first time you actually need it. This is useful when a value is expensive to build, depends on another property that is not ready during initialization, or may never be used at all.
Quick answer: A lazy property is a stored property whose initial value is computed the first time it is accessed. In Swift, it must be declared with lazy var, not let, because Swift needs to write the value later.
Difficulty: Beginner
You'll understand this better if you know: how stored properties work, how Swift initializers set values, and the difference between let and var.
1. What Is Swift Lazy Properties?
A lazy property is a stored property that does not get its initial value during object creation. Instead, Swift waits until the first access, then runs the initialization code once and stores the result.
- It delays work until the value is needed.
- It is defined on a class, struct, or enum as a stored property.
- It is especially useful for expensive setup or values that depend on other instance properties.
- It is initialized only once, on first access.
In practice, lazy properties are a way to trade immediate startup cost for on-demand computation.
2. Why Swift Lazy Properties Matter
Not every value needs to exist the moment an instance is created. Some values are expensive to compute, and some are never used. A lazy property can make initialization faster and reduce unnecessary work.
They also help when one property needs access to another property that is set during initialization. Because the lazy value is not created until later, it can safely read instance state that would otherwise not be available early enough.
Use lazy properties when deferred work improves clarity or performance. Do not use them just to avoid writing an initializer, because that can make code harder to reason about.
3. Basic Syntax or Core Idea
A lazy property is declared with the lazy keyword and var. Swift does not allow lazy properties to be constants, because the value must be assigned after initialization has completed.
Minimal example
This example shows the basic pattern: a property whose value is created only when first requested.
class DataStore {
lazy var cache: [String: Int] = {
print("Building cache...")
return ["apples": 3, "bananas": 5]
}()
}The closure runs only when cache is accessed for the first time. After that, Swift stores the result and reuses it.
Why the closure is used
The initializer is usually written as a closure because it lets you put several setup steps in one place and then return the final value.
4. Step-by-Step Examples
Example 1: Deferring expensive setup
Imagine a view model that builds a large lookup table. If the table is not always needed, lazy initialization avoids doing that work too early.
final class LookupViewModel {
lazy var lookupTable: [Int: String] = {
var table: [Int: String] = [:]
for number in 1...3 {
table[number] = "Item \(number)"
}
return table
}()
}This is useful when the table might never be queried. The initialization cost happens only if the property is accessed.
Example 2: Using other instance properties
A lazy property can read values that were assigned during initialization. That makes it useful for dependent setup.
struct Report {
var title: String
var body: String
lazy var summary: String = {
return "\(title): \(body.prefix(20))..."
}()
}Here, summary can safely use title and body because those stored properties are already set before the first access.
Example 3: A lazily created helper object
Sometimes a property exists only to support another property or method. Creating that helper lazily keeps startup light.
final class ImageProcessor {
lazy var formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter
}()
func formattedCount(_ count: Int) -> String {
return formatter.string(from: NSNumber(value: count)) ?? "0"
}
}The formatter is created only if formatting is actually needed.
Example 4: First access and reuse
This example shows that the initializer runs once, not every time the property is read.
final class CounterExample {
lazy var token: String = {
print("Generating token")
return "ABC-123"
}()
}
let example = CounterExample()
print("Before access")
print(example.token)
print(example.token)The message prints only on the first access. The second read returns the stored value immediately.
5. Practical Use Cases
- Building caches, lookup tables, or derived data that may never be used.
- Creating helper objects such as formatters, parsers, or managers on demand.
- Deferring expensive file parsing or image preparation until needed.
- Setting up values that depend on other instance properties after initialization.
- Reducing startup work in objects that are created often but used lightly.
Lazy properties are most valuable when the cost of creating a value is noticeable or when that value is only used in some code paths.
6. Common Mistakes
Mistake 1: Using lazy with let
Swift requires a lazy property to be mutable because the value is assigned later. A constant cannot be written after initialization.
Problem: This declaration is invalid because let properties cannot be assigned lazily after the instance is created.
class Session {
lazy let id: String = {
return "session-1"
}()
}Fix: Use lazy var instead.
class Session {
lazy var id: String = {
return "session-1"
}()
}The corrected version works because Swift can assign the value later the first time it is needed.
Mistake 2: Expecting lazy to work with a local variable
Lazy stored properties belong to types, not to ordinary local constants or variables inside a function body.
Problem: This code tries to use lazy in a local scope, which Swift does not allow.
func makeValue() -> Int {
lazy var value = 10
return value
}Fix: Use a normal variable, or move the lazy property onto a type if deferred storage is really needed.
func makeValue() -> Int {
let value = 10
return value
}The fix works because local values should be created directly instead of lazily stored.
Mistake 3: Forgetting that the initializer can run more than once in concurrent access scenarios
Lazy initialization is not a general-purpose synchronization tool. If multiple threads access the same instance at the same time, you should not assume the closure is automatically protected against every race in every design.
Problem: If your lazy initializer has side effects, relying on it as though it were a lock can lead to surprising behavior in concurrent code.
final class LoggerHolder {
lazy var logger: String = {
print("Setting up logger")
return "ready"
}()
}Fix: Keep lazy initializers free of important side effects, or protect shared access with your own synchronization strategy when concurrency matters.
final class LoggerHolder {
private let logger: String
init() {
logger = "ready"
}
}The corrected version avoids hidden work during access and makes the initialization path explicit.
7. Best Practices
Practice 1: Use lazy for expensive values, not ordinary ones
If a value is cheap and always needed, make it a regular stored property. Lazy initialization adds complexity, so it should solve a real problem.
final class Profile {
let name: String
lazy var displayName: String = name.uppercased()
init(name: String) {
self.name = name
}
}If displayName is always needed, a computed property or a normal stored property is often clearer.
Practice 2: Keep the lazy closure small and focused
A lazy initializer should build one value, not perform unrelated setup. Small closures are easier to read and debug.
final class FeedController {
lazy var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter
}()
}When a closure does one job, it is easier to verify that the value is created correctly and only when needed.
Practice 3: Avoid depending on lazy properties during initialization
Lazy properties are meant to be accessed after the instance is fully initialized. Keep the initializer responsible for essential state and let lazy properties handle optional or deferred work.
final class Document {
let title: String
lazy var slug: String = {
return title.lowercased().replacingOccurrences(of: " ", with: "-")
}()
init(title: String) {
self.title = title
}
}This keeps initialization simple and makes the deferred behavior easy to understand.
8. Limitations and Edge Cases
- Lazy properties must be stored properties; they cannot be computed properties.
- They must be declared with var, not let.
- The first access performs initialization, so any side effects in the closure happen at that moment, not during object creation.
- For value types like structs, accessing or mutating a lazy property can require a mutable instance.
- Lazy initialization can make performance more predictable, but it can also shift work to an unexpected time if access happens in a hot path.
- Accessing a lazy property after copying a value type may initialize the property separately on each copy, because each copy owns its own storage.
Note: Lazy properties are often associated with classes, but they can also be used with value types as long as the instance is mutable when the property is first accessed.
9. Practical Mini Project
Here is a small complete example of a report generator that creates a formatted summary only when it is requested.
struct SalesReport {
let month: String
let revenue: Int
let cost: Int
lazy var summary: String = {
let profit = revenue - cost
return "\(month): revenue \(revenue), cost \(cost), profit \(profit)"
}()
}
var report = SalesReport(month: "June", revenue: 1200, cost: 800)
print("Report created")
print(report.summary)This project demonstrates a simple pattern: create a value type with a deferred derived property, then read it only when needed. The summary is not built until report.summary is accessed.
10. Key Points
- Lazy properties defer work until first access.
- Use lazy var for values that are expensive or optional to create.
- They are useful when a property depends on other instance state.
- The initializer closure runs once and the result is stored.
- Do not use lazy properties as a substitute for good initializer design.
11. Practice Exercise
- Create a UserProfile type with firstName, lastName, and a lazy fullName property.
- Make fullName combine the two names only when it is first accessed.
- Print the profile data and access fullName twice to confirm the value is reused.
Expected output: the combined full name should appear both times, but the construction logic should run only once.
Hint: put the name-building code inside a closure assigned to lazy var.
Solution:
struct UserProfile {
let firstName: String
let lastName: String
lazy var fullName: String = {
return "\(firstName) \(lastName)"
}()
}
var profile = UserProfile(firstName: "Ava", lastName: "Stone")
print(profile.fullName)
print(profile.fullName)12. Final Summary
Swift lazy properties are a simple way to defer stored property initialization until the moment a value is actually needed. They are especially helpful for expensive setup, derived values, and helper objects that may never be used.
Use them carefully: declare them as lazy var, keep the initializer focused, and choose them only when deferred work genuinely improves your design. For small or always-needed values, a regular stored property is usually clearer.
Once you are comfortable with lazy properties, the next useful topic is how they compare with computed properties and regular stored properties in Swift.