| name | bitmap-and-image-optimization |
| description | Downsample, cache, and choose the right image format (AVIF, HEIC, WebP) to cut memory, network, and decode time on iOS and Android. |
Bitmap and Image Optimization
Instructions
Images dominate mobile memory and bandwidth. A single 4000×3000 JPEG decoded naively takes 48 MB in RAM (4000 × 3000 × 4 bytes). This skill covers sizing, caching, and format selection.
1. Size Images to the Display Target, Not the Source
Decode to the on-screen pixel size, not the full resolution. The screen does not care about extra pixels.
Android (Glide):
Glide.with(view)
.load(url)
.override(view.width, view.height)
.format(DecodeFormat.PREFER_RGB_565)
.into(view)
Android (Coil):
imageView.load(url) {
size(ViewSizeResolver(imageView))
allowRgb565(true)
}
iOS (SDWebImage):
imageView.sd_setImage(
with: URL(string: url),
placeholderImage: placeholder,
context: [.imageThumbnailPixelSize: CGSize(width: 512, height: 512)]
)
iOS native UIImage downsample:
func downsample(url: URL, to size: CGSize, scale: CGFloat) -> UIImage? {
let opts: [CFString: Any] = [kCGImageSourceShouldCache: false]
guard let src = CGImageSourceCreateWithURL(url as CFURL, opts as CFDictionary) else { return nil }
let maxDim = max(size.width, size.height) * scale
let thumbOpts: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDim,
]
return CGImageSourceCreateThumbnailAtIndex(src, 0, thumbOpts as CFDictionary).map { UIImage(cgImage: $0) }
}
Flutter:
CachedNetworkImage(
imageUrl: url,
memCacheWidth: 512, // decode at target size
memCacheHeight: 512,
placeholder: (c, _) => const ColoredBox(color: Color(0xffeeeeee)),
)
React Native — prefer FastImage or Expo Image; they downsample natively. Set resizeMode and explicit dimensions.
2. Format Selection (2026 defaults)
| Use case | Best format | Why |
|---|
| Photos over network | AVIF | 30–50% smaller than JPEG; wide OS support (Android 12+, iOS 16+). |
| Fallback for AVIF | WebP | Universal, better than JPEG/PNG, lossy or lossless. |
| Apple-first pipelines | HEIC | Native to iOS; smaller than JPEG. |
| Illustrations / icons | SVG or vector drawable | Scale for free; typically tiny. |
| Flat UI with transparency | WebP lossless or PNG-8 | Smaller than PNG-24. |
| Animations | AVIF sequence / AVIS or animated WebP | Replace GIFs. |
Content-Negotiation from your CDN:
Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8
Return AVIF when Accept contains image/avif. Emit Vary: Accept.
3. Bitmap Config (Android)
- Use
RGB_565 (2 bytes/px) for opaque photos on low-end devices — halves RAM vs ARGB_8888.
- Use
HARDWARE bitmaps on API 26+ — zero-copy to GPU, but cannot be read back on CPU.
BitmapFactory.Options().apply {
inPreferredConfig = Bitmap.Config.HARDWARE
inSampleSize = calculateInSampleSize(width, height, targetW, targetH)
}
4. Caching
Every serious loader has a memory cache + disk cache. Size them generously:
- Memory cache: 20–25% of
Runtime.getRuntime().maxMemory() (Android) or 50 MB cap (iOS).
- Disk cache: 128–256 MB; keyed by final transformed URL so variant decoding is hit, not recomputed.
Coil example:
ImageLoader.Builder(context)
.memoryCache { MemoryCache.Builder(context).maxSizePercent(0.25).build() }
.diskCache { DiskCache.Builder().directory(context.cacheDir.resolve("img")).maxSizeBytes(256L * 1024 * 1024).build() }
.respectCacheHeaders(true)
.build()
5. Preload Critical Images
For hero images on the next screen, preload before navigation:
imageLoader.enqueue(ImageRequest.Builder(ctx).data(heroUrl).build())
In SwiftUI use ImageRenderer or AsyncImage with .task { await prefetch() }. In Flutter use precacheImage.
6. Avoid Common Footguns
- Do not decode images on the UI/main thread. All major loaders do this for you; do not reimplement.
- Do not store bitmaps in
StateFlow / @State. Store identifiers; let the loader return bitmaps per view.
- Do not apply
BackdropFilter / blur over large images; blur once into a smaller buffer, then upscale.
- Placeholder-shimmer components should be cheap —
Modifier.background or a single gradient.
7. Verify
- Android:
adb shell dumpsys meminfo --package <pkg> | findstr Graphics shows GPU texture memory.
- iOS: Debug → Rendering → Color Misaligned Images (a misaligned image reports a yellow tint = slow path).
- Flutter: DevTools → Memory → filter by
Image and ImageCache.
8. Production Metrics
Log per image: URL, source (cache/network), decoded width/height, decode time. A p95 > 40 ms or source=network > 50% on warm scrolls is a regression.
Checklist