iPhone image caching
asynchronous caching
iOS development
mobile app performance
image optimization

What's the best approach to asynchronous image caching on the iPhone?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Asynchronous image caching is really a coordination problem between networking, decoding, memory usage, and cell reuse. The best approach is usually a layered one: use URLSession for background fetch, an in-memory cache for fast reuse, a disk cache for persistence, and careful UI updates so reused cells do not display the wrong image. Most bugs in this area come from lifecycle mistakes rather than from the cache container itself.

Use Memory Cache for Fast Reuse

NSCache is the natural in-memory cache on iOS because it can evict items automatically under memory pressure.

swift
1import UIKit
2
3final class ImageMemoryCache {
4    static let shared = NSCache<NSURL, UIImage>()
5}

This should be the first place you look before starting a new network request. Memory cache is much faster than disk or network access, so it is what keeps scrolling smooth when images are revisited.

Download Asynchronously with URLSession

The network part should happen off the main thread, with UI updates marshaled back to the main queue.

swift
1import UIKit
2
3func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
4    if let cached = ImageMemoryCache.shared.object(forKey: url as NSURL) {
5        completion(cached)
6        return
7    }
8
9    URLSession.shared.dataTask(with: url) { data, _, _ in
10        guard let data, let image = UIImage(data: data) else {
11            DispatchQueue.main.async { completion(nil) }
12            return
13        }
14
15        ImageMemoryCache.shared.setObject(image, forKey: url as NSURL)
16        DispatchQueue.main.async { completion(image) }
17    }.resume()
18}

This is the minimum viable pattern: cache hit first, async fetch second, UI update on the main queue last.

Account for Cell Reuse

Table and collection view cells are reused. If an old request finishes after the cell has been reassigned to different data, the wrong image can appear in the wrong row.

That is why image loading code should either cancel stale requests, compare the current expected URL before assigning the image, or use a dedicated image-loading abstraction that handles reuse safely.

The cache is only half the system. Correct binding between model and cell matters just as much.

Disk Cache Helps Across Launches

Memory cache disappears when the app terminates or when the system purges it. If images are expensive to re-download, a disk cache layer is useful.

In production, this is often handled by an image library or by a small custom cache that stores image data under a hashed file name. The important design idea is to check memory first, disk second, and network last.

That ordering keeps the interface responsive while still giving persistence across launches.

Avoid Heavy Work on the Main Thread

Downloading is not the only expensive part. Image decoding and resizing can also hurt scrolling performance if done at the wrong time.

If the app shows many large remote images, pre-sizing or decoding them off the main thread can be just as important as caching them. Otherwise the network layer looks innocent while the UI still stutters.

Libraries Are Often Worth It

For many production apps, the best approach is not writing the entire pipeline by hand. Established libraries exist because request deduplication, cancellation, resizing, memory pressure, and disk persistence are harder than they first appear.

Even if you use a library, though, the architecture is still the same: async fetch, cache hierarchy, and safe UI binding.

Common Pitfalls

  • Starting duplicate downloads because no cache lookup happens before the request.
  • Updating a reused cell with a stale image result.
  • Treating memory cache as if it were persistent storage.
  • Doing image decoding or resizing work on the main thread.
  • Assuming the cache alone fixes performance without considering cell lifecycle and cancellation.

Summary

  • Use a layered design with async networking, memory cache, and often disk cache.
  • 'NSCache is a good in-memory starting point on iOS.'
  • Always account for cell reuse and stale completion handlers.
  • Keep network and image processing work off the main thread.
  • For production apps, a mature image-loading library is often the pragmatic choice.

Course illustration
Course illustration

All Rights Reserved.