iOS used to be hyper memory efficient. Somewhere along the line, that is no longer the case. The same with App responsiveness as well. With Apps size increasing every year.
Back on the iPhone 3G, PNG decoding alone was slow enough that I had to cache pre-rendered bitmap representations into SQLite blobs to achieve "glassy" scrolling of image-heavy grids; I'd do the SQLite loading on a worker thread and dump the pre-rendered bitmap representation right back into a CGImage wrapper. I recall there being some inline NEON assembly involved as well.
Within 1-2 years, those optimizations were totally unnecessary.
I've noticed that iOS 18+ seems to issue low-memory warnings more frequently than older iOS versions (at least on devices with <=4GB RAM). It can now happen with the app is using less than half the device's RAM, which I don't recall seeing before. I assume this is because the OS is using more RAM for AI and such.
Yes, that matches my observations. I develop photography software and in early versions of iOS 18, sometimes saving a large image to the camera roll would fail because the daemon in charge of carrying out the photo library transactions would get killed due to low memory. This only happened on devices that ran Apple intelligence. Fortunately they seemed to have fixed that bug around 18.1 or 18.2 if I recall.
A key tip that wasn't in the blog post: be extremely judicious about taking on new code dependencies.
For many apps the main source of memory usage isn't stack or heap memory consumed during runtime, it's the loading of the binary itself into memory. I've seen some wild app binary sizes (200MB+ for relatively modest apps without that much functionality).
One thing that's endlessly frustrating about mobile dev is that the vast majority of devs are thoughtless about dependencies, how they are built/linked, and how that impacts memory use. Far and away the dominant mode of dependencies in mobile-land is just "statically link it and forget about it".
This is the singular biggest contributor to app size bloat, and pretty up there for runtime memory consumption as well.
A double whammy here is that modern iOS apps are actually multiple binaries (main app, watch extensions, widget extensions, etc.), and if you heavily use static linking you're carrying multiple copies of the bloat.
A few actionable things:
- Be very, very judicious about taking on new dependencies. Is a library offering enough value and marginal functionality to be worth the weight? (e.g., I don't think AFNetworking in 2025 meets the bar for the vast majority of people, but yet it's still everywhere?)
- Dynamically link, especially if you're a complex app with multiple targets.
- Deadstrip aggressively. I cannot emphasize this enough. The default compiler settings will not deadstrip unused public symbols in static libraries. Fix this.
7 comments
[ 0.27 ms ] story [ 13.4 ms ] threadFastImageCache uses memory mapping and is very efficient, but it's 10 years old: https://github.com/path/FastImageCache
If anyone wants to build a modern rkyv + FastImageCache hybrid…
Within 1-2 years, those optimizations were totally unnecessary.
I'd love to see a specific example with before/after numbers.
> Lottie [...] ends up loading every frame as a raw bitmap.
Isn't Lottie rendering the vector data directly to screen?
For many apps the main source of memory usage isn't stack or heap memory consumed during runtime, it's the loading of the binary itself into memory. I've seen some wild app binary sizes (200MB+ for relatively modest apps without that much functionality).
One thing that's endlessly frustrating about mobile dev is that the vast majority of devs are thoughtless about dependencies, how they are built/linked, and how that impacts memory use. Far and away the dominant mode of dependencies in mobile-land is just "statically link it and forget about it".
This is the singular biggest contributor to app size bloat, and pretty up there for runtime memory consumption as well.
A double whammy here is that modern iOS apps are actually multiple binaries (main app, watch extensions, widget extensions, etc.), and if you heavily use static linking you're carrying multiple copies of the bloat.
A few actionable things:
- Be very, very judicious about taking on new dependencies. Is a library offering enough value and marginal functionality to be worth the weight? (e.g., I don't think AFNetworking in 2025 meets the bar for the vast majority of people, but yet it's still everywhere?)
- Dynamically link, especially if you're a complex app with multiple targets.
- Deadstrip aggressively. I cannot emphasize this enough. The default compiler settings will not deadstrip unused public symbols in static libraries. Fix this.