Why Senior iOS Developers Always Use Lazy Properties (And You Should Too)
Master lazy properties to slash app startup time and memory usage
Swift Essentials

I’ve been reviewing iOS codebases for years, and there’s one pattern I see consistently in apps built by senior developers: strategic use of lazy properties. While junior developers tend to initialize everything upfront “just to be safe,” experienced developers understand that not everything needs to be ready immediately.
The difference is dramatic. Apps that take 3–4 seconds to launch can often be reduced to under a second just by making expensive objects lazy. Yet most developers either don’t know about lazy properties or avoid them because they seem “complicated.”
They’re not complicated. And once you understand when and how to use them, you’ll wonder how you ever built apps without them.
📺 Coming soon to Swift Pal: A complete video series on iOS performance optimization at _https://youtube.com/@swift-pal_
🤷♂️ What Are Lazy Properties Actually?
Most developers think lazy properties are just “variables that initialize later.” That’s like saying a Ferrari is just “a car that goes fast” — technically true, completely missing the point.
Lazy properties are Swift’s way of saying “don’t do expensive work until someone actually needs the result.” They’re calculated once, the first time you access them, then cached forever.
// Regular property - calculated at object creation
class PhotoEditor {
let expensiveProcessor = HeavyImageProcessor() // Runs immediately
init() {
// HeavyImageProcessor() already ran by the time we get here
print("PhotoEditor created")
}
}
// Lazy property - calculated on first access
class PhotoEditor {
lazy var expensiveProcessor = HeavyImageProcessor() // Runs when first accessed
init() {
print("PhotoEditor created instantly")
}
}
let editor = PhotoEditor() // Fast!
// HeavyImageProcessor not created yet...
editor.expensiveProcessor.process(image) // NOW it gets createdThe key insight: lazy properties let you separate object creation from expensive computation. Your objects initialize fast, but heavy work only happens when actually needed.
🚀 Real iOS Scenarios Where Lazy Properties Shine
Let me show you the exact scenarios where lazy properties transform app performance:
Expensive UI Components
class CustomViewController: UIViewController {
// ❌ Bad: Creates complex view immediately
let chartView = ComplexChartView()
let animationView = LottieAnimationView()
let customDrawingView = ExpensiveCustomView()
override func viewDidLoad() {
super.viewDidLoad()
// All three views already created, even if user never sees them
}
}
// ✅ Better: Create views only when needed
class CustomViewController: UIViewController {
lazy var chartView: ComplexChartView = {
let view = ComplexChartView()
view.setupComplexConfiguration()
return view
}()
lazy var animationView: LottieAnimationView = {
let view = LottieAnimationView(name: "animation")
view.loopMode = .loop
view.contentMode = .scaleAspectFit
return view
}()
lazy var customDrawingView: ExpensiveCustomView = {
let view = ExpensiveCustomView()
view.setupExpensiveDrawingCode()
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
// Views created only when accessed
}
@IBAction func showChart(_ sender: UIButton) {
view.addSubview(chartView) // Created here, not at viewDidLoad
chartView.animateIn()
}
}Network and Data Managers
class AppCoordinator {
// ❌ Bad: Creates all managers at app launch
let networkManager = NetworkManager()
let cacheManager = ImageCacheManager()
let analyticsManager = AnalyticsManager()
let locationManager = LocationManager()
init() {
// All managers created even if app never uses them
// Location permission popup shows immediately
// Network connections established unnecessarily
}
}
// ✅ Better: Create managers on demand
class AppCoordinator {
lazy var networkManager: NetworkManager = {
let manager = NetworkManager()
manager.configure(with: APIConfiguration.production)
return manager
}()
lazy var cacheManager: ImageCacheManager = {
let manager = ImageCacheManager()
manager.setupDiskCache()
return manager
}()
lazy var analyticsManager: AnalyticsManager = {
let manager = AnalyticsManager()
manager.configure(apiKey: "your-key")
return manager
}()
lazy var locationManager: LocationManager = {
let manager = LocationManager()
manager.requestPermissions() // Only when actually needed
return manager
}()
func startNetworking() {
networkManager.start() // Created here
}
func trackEvent(_ event: String) {
analyticsManager.track(event) // Created here
}
}Core Data and Database Setup
class DataController {
// ❌ Bad: Sets up Core Data stack immediately
let persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DataModel")
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Core Data error: \(error)")
}
}
return container
}()
init() {
// Core Data stack loads during init - blocks app launch
}
}
// ✅ Better: Load Core Data when first database operation happens
class DataController {
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DataModel")
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Core Data error: \(error)")
}
}
return container
}()
var context: NSManagedObjectContext {
return persistentContainer.viewContext // Core Data loads here
}
func saveContext() {
if context.hasChanges {
try? context.save()
}
}
}📊 Performance Impact
Here’s what lazy properties do for your app:
Memory Usage
// Eager initialization - all objects created immediately
class EagerViewController: UIViewController {
let imageProcessor = HeavyImageProcessor() // Created at init
let dataParser = ComplexDataParser() // Created at init
let mlModel = MLModelLoader() // Created at init
// All three objects consume memory immediately
}
// Lazy initialization - objects created when first accessed
class LazyViewController: UIViewController {
lazy var imageProcessor = HeavyImageProcessor() // Created when needed
lazy var dataParser = ComplexDataParser() // Created when needed
lazy var mlModel = MLModelLoader() // Created when needed
// Objects only consume memory when actually used
}Startup Time
The more expensive objects you create during app launch, the slower your startup becomes. Lazy properties defer this work until the objects are actually needed.
📝 Advanced Lazy Property Patterns
Once you master basic lazy properties, here are the advanced patterns that separate good developers from great ones:
Lazy Computed Properties
class UserProfile {
let firstName: String
let lastName: String
let email: String
let joinDate: Date
// Regular computed property - calculated every time
var displayName: String {
return "\(firstName) \(lastName)"
}
// Lazy stored property - calculated once, cached forever
lazy var formattedJoinDate: String = {
let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .none
return formatter.string(from: joinDate)
}()
// Complex lazy computation
lazy var userThumbnail: UIImage = {
return generateUserThumbnail(
name: displayName,
email: email,
size: CGSize(width: 40, height: 40)
)
}()
init(firstName: String, lastName: String, email: String, joinDate: Date) {
self.firstName = firstName
self.lastName = lastName
self.email = email
self.joinDate = joinDate
}
}Lazy Dependency Injection
protocol NetworkServiceProtocol {
func fetchData() -> Data?
}
class NetworkService: NetworkServiceProtocol {
func fetchData() -> Data? {
// Actual network implementation
return nil
}
}
class MockNetworkService: NetworkServiceProtocol {
func fetchData() -> Data? {
// Mock data for testing
return Data()
}
}
class DataManager {
// Lazy dependency injection - service created when first used
lazy var networkService: NetworkServiceProtocol = {
#if DEBUG
return MockNetworkService()
#else
return NetworkService()
#endif
}()
func loadUserData() {
let data = networkService.fetchData() // Service created here
// Process data
}
}Lazy Collections and Sequences
class ImageGallery {
let imageURLs: [URL]
// Lazy loading of actual images
lazy var images: [UIImage] = {
return imageURLs.compactMap { url in
return UIImage(contentsOfFile: url.path)
}
}()
// Lazy filtered collections
lazy var thumbnails: [UIImage] = {
return images.map { image in
return image.resized(to: CGSize(width: 100, height: 100))
}
}()
// Lazy expensive computations
lazy var imageMetadata: [ImageMetadata] = {
return images.map { image in
return ImageMetadata(
size: image.size,
colorProfile: image.colorSpace,
orientation: image.imageOrientation
)
}
}()
init(imageURLs: [URL]) {
self.imageURLs = imageURLs
// No images loaded yet!
}
}Lazy Singletons (The Right Way)
// ❌ Don't do this - not thread safe
class BadSingleton {
static var shared: BadSingleton?
static func getInstance() -> BadSingleton {
if shared == nil {
shared = BadSingleton()
}
return shared!
}
}
// ✅ Better - thread safe lazy singleton
class AudioManager {
static let shared = AudioManager() // Lazy by default in Swift
private lazy var audioEngine: AVAudioEngine = {
let engine = AVAudioEngine()
engine.prepare()
return engine
}()
private lazy var audioSession: AVAudioSession = {
let session = AVAudioSession.sharedInstance()
try? session.setCategory(.playback)
return session
}()
private init() {} // Prevent external instantiation
}
// ✅ Even better - lazy service locator
class ServiceLocator {
static let shared = ServiceLocator()
lazy var analyticsService: AnalyticsService = {
return AnalyticsService(apiKey: Configuration.analyticsKey)
}()
lazy var crashReportingService: CrashReportingService = {
return CrashReportingService(apiKey: Configuration.crashlyticsKey)
}()
lazy var userDefaultsService: UserDefaultsService = {
return UserDefaultsService(suiteName: Configuration.appGroup)
}()
private init() {}
}⚠️ When Lazy Properties Go Wrong
Lazy properties aren’t magic — they can cause problems if you’re not careful:
Thread Safety Issues
class ThreadUnsafeExample {
lazy var expensiveResource = ExpensiveResource()
func accessFromMultipleThreads() {
DispatchQueue.global().async {
_ = self.expensiveResource // Thread 1
}
DispatchQueue.global().async {
_ = self.expensiveResource // Thread 2
}
// Race condition! ExpensiveResource might be created twice
}
}
// ✅ Thread-safe alternatives
class ThreadSafeExample {
private let queue = DispatchQueue(label: "lazy-init")
private var _expensiveResource: ExpensiveResource?
var expensiveResource: ExpensiveResource {
return queue.sync {
if _expensiveResource == nil {
_expensiveResource = ExpensiveResource()
}
return _expensiveResource!
}
}
}
// ✅ Or use a different pattern entirely
class ThreadSafeAlternative {
static let expensiveResource = ExpensiveResource() // Thread-safe by default
}When NOT to Use Lazy Properties
// ❌ Don't use lazy for simple values
class BadExample {
lazy var userName = "DefaultUser" // Pointless - String is cheap
lazy var isEnabled = true // Pointless - Bool is cheap
lazy var maxCount = 100 // Pointless - Int is cheap
}
// ❌ Don't use lazy for things you always need
class ViewController: UIViewController {
lazy var titleLabel = UILabel() // Bad - you always need this
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(titleLabel) // Always accessed anyway
}
}
// ❌ Don't use lazy for things that must be ready immediately
class AudioPlayer {
lazy var audioSession = AVAudioSession.sharedInstance() // Bad!
func playEmergencyAlert() {
// Too late to set up audio session now!
audioSession.setActive(true)
}
}
// ✅ Use lazy for expensive, conditional, or optional operations
class GoodExample {
lazy var complexCalculation: Double = {
return (0...1000000).reduce(0) { $0 + sin(Double($1)) }
}()
lazy var optionalFeature: AdvancedFeature? = {
guard Configuration.advancedFeaturesEnabled else { return nil }
return AdvancedFeature()
}()
}Memory Leaks and Retain Cycles
class LeakyExample {
lazy var processor: DataProcessor = {
let processor = DataProcessor()
processor.delegate = self // Retain cycle!
return processor
}()
}
// ✅ Fixed version
class FixedExample {
lazy var processor: DataProcessor = {
let processor = DataProcessor()
processor.delegate = self
return processor
}()
deinit {
processor.delegate = nil // Break the cycle
}
}
// ✅ Better - use weak references
class BetterExample {
lazy var processor: DataProcessor = {
let processor = DataProcessor()
processor.weakDelegate = self // Weak reference
return processor
}()
}🛠️ Practical Patterns You Can Use Today
Here are some copy-paste patterns that work great in real apps:
Lazy Table View Cells
class ComplexTableViewCell: UITableViewCell {
lazy var customImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 8
return imageView
}()
lazy var gradientLayer: CAGradientLayer = {
let layer = CAGradientLayer()
layer.colors = [UIColor.clear.cgColor, UIColor.black.cgColor]
layer.locations = [0.0, 1.0]
return layer
}()
override func prepareForReuse() {
super.prepareForReuse()
customImageView.image = nil
// Lazy properties stay initialized but content gets reset
}
}Lazy Configuration Objects
class AppConfiguration {
lazy var developmentConfig: Configuration = {
var config = Configuration()
config.apiBaseURL = "https://dev-api.example.com"
config.loggingLevel = .verbose
config.analyticsEnabled = false
return config
}()
lazy var productionConfig: Configuration = {
var config = Configuration()
config.apiBaseURL = "https://api.example.com"
config.loggingLevel = .error
config.analyticsEnabled = true
return config
}()
var currentConfig: Configuration {
#if DEBUG
return developmentConfig
#else
return productionConfig
#endif
}
}Lazy properties are a straightforward way to improve app performance. By deferring expensive object creation until it’s actually needed, you can reduce startup time and memory usage.
The concept is simple: if an object is expensive to create and might not be used immediately, make it lazy. Your app will launch faster and use less memory.
Look through your existing code for heavy objects that don’t need to be ready at startup. Convert them to lazy properties and see the improvement.
📺 For more iOS performance tips, check out Swift Pal: _https://youtube.com/@swift-pal_
🎉 Enjoyed this article? Your support means the world to me!
💬 Drop a comment below! I love hearing about your experiences and answering questions
🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal
💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal
Happy coding! 🚀
New articles, straight to your inbox.
No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.
Keep reading
Why I'm Rebuilding My Blog From Scratch (and Leaving Medium)
After years of publishing on someone else's platform, I'm moving my writing to a home I actually own. Here's the reasoning, and what I'm building instead.
ReadData Is the Model: The Most Ignored Part of AI
A beginner-friendly guide to why data quality beats model hype.
Read