Optimizing Image Loading in Mobile Apps
Loading images is one of those tasks easy to make "working" and very hard to make right. App loads photos, shows them — all normal. Until user opens gallery with 200 items, and Bitmap allocation in Android Profiler starts drawing curve: 4, 8, 14, 23 MB... Then OOM. Or — softer scenario — iPhone 12 users in Low Data Mode wait 4–6 seconds for first image because original 4K loads instead of preview.
Key Problems
Wrong image size. Server returns original 2400×3200, ImageView — 80×80 dp. Glide / Kingfisher / Coil do downsampling, but first these 29 MB come over network and decode in memory. On Android BitmapFactory.Options.inSampleSize or URL resize params (?w=160&h=160&fit=crop) solve it before loading.
Missing disk cache. By default SDWebImage caches to disk, but if someone set SDWebImageOptions.refreshCached for "freshness" — each app launch reloads all images. Screens with user avatars mean 20–30 extra network requests each opening.
Sequential loading instead of parallel. Custom URLSession.dataTask implementations often create queue where next request starts after previous finishes. In 10-element list — waiting sum of all 10 RTT instead of maximum.
How We Solve
On iOS default stack: Kingfisher for Swift projects, SDWebImage for Obj-C legacy or CocoaPods-dependent projects. Kingfisher convenient KFImage in SwiftUI and native @MainActor support. Key settings:
KingfisherManager.shared.cache.diskStorage.config.sizeLimit = 500 * 1024 * 1024 // 500 MB
KingfisherManager.shared.cache.memoryStorage.config.totalCostLimit = 100 * 1024 * 1024 // 100 MB
For progressive JPEG loading — ImageDataProcessor with ProgressiveJPEGAddon. User sees blurry image immediately, not placeholder 2 seconds.
On Android: Coil for Compose projects (native AsyncImage), Glide for View-based. Glide knows thumbnail(0.1f) — loads 10% of original size as placeholder while loading full version. For WebP conversion on server, Glide handles it out-of-box, Coil requires SvgDecoder / VideoFrameDecoder via separate dependencies.
Mandatory pattern for both platforms: parameterized URLs with size for specific ImageView. If backend on Cloudinary or imgproxy — pass ?width={viewWidthDp * density}&format=webp&quality=80. WebP 25–35% smaller than JPEG at same visual quality for photos.
Placeholder Strategy
Empty gray rectangle — bad. BlurHash or ThumbHash — good. Compact (20–30 bytes) hash of image rendering locally as colored blurry preview before loading real content. On iOS — BlurHash library, on Android — io.github.nicklockwood:thumbhash. Hash data comes with JSON response — zero network cost for preview.
Case: Carousel with 50 Images
Client implemented UIScrollView with UIImageView via page control. On opening — immediately loaded all 50 images. On slow 3G WKWebView (yes, there was such hybrid) closed from OOM after 3–4 swipes.
Solution: lazy loading via UIPageViewController with visibility window ±2 pages. NSCache with 20 MB limit for decoded images. Rest — only URL in memory. Time to first interaction dropped from 8 to 1.2 seconds.
Timelines
Image loading audit and library setup — 1–3 days. With CDN resize integration and BlurHash throughout app — 1 week.







