Bitmap and image optimization
Agent skills for profiling and optimizing mobile app performance (startup, memory, frame-rate, network).
npx -y skills add almasumdev/awesome-mobile-performance-agent-skills --skill bitmap-and-image-optimizationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Downsample, cache, and choose the right image format (AVIF, HEIC, WebP) to cut memory, network, and decode time on iOS and Android.
SKILL.md
5.8 KB, as published. Nobody here has run it
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) // decode to target size
.format(DecodeFormat.PREFER_RGB_565) // for opaque images
.into(view)
Android (Coil):
imageView.load(url) {
size(ViewSizeResolver(imageView)) // matches the view
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
HARDWAREbitmaps on API 26+ — zero-copy to GPU, but cannot be read back on CPU.
BitmapFactory.Options().apply {
inPreferredConfig = Bitmap.Config.HARDWARE // API 26+
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.backgroundor a single gradient.
7. Verify
- Android:
adb shell dumpsys meminfo --package <pkg> | findstr Graphicsshows GPU texture memory. - iOS: Debug → Rendering → Color Misaligned Images (a misaligned image reports a yellow tint = slow path).
- Flutter: DevTools → Memory → filter by
ImageandImageCache.
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
- Images decoded at view/display size, not source size.
- Opaque photos use RGB_565 (Android) / thumbnail downsample (iOS).
- AVIF or WebP served with
Acceptcontent negotiation; JPEG/PNG only as fallback. - Memory cache sized to 20–25% of heap; disk cache 128–256 MB.
- Hero images preloaded before navigation.
- No image decoding on main/UI thread.
- CI dashboard tracks p95 image decode time and bytes/screen.