Bitmap and image optimization
Downsample, cache, and choose the right image format (AVIF, HEIC, WebP) to cut memory, network, and decode time on iOS and Android.From its SKILL.md
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.
SKILL.md
5.8 KB, ~1.5k tokens by cl100k_base, 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.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.