Images are the #1 cause of APK bloat and the #1 network payload for most apps. Choosing the right format and delivery strategy directly impacts download size, render time, and memory.
WebP: Replace PNG and JPEG
WebP provides better compression than both PNG and JPEG at the same visual quality:
| Format | Lossless | Lossy | Typical size vs PNG |
|---|---|---|---|
| PNG | ✓ | — | Baseline |
| JPEG | — | ✓ | ~60% of PNG |
| WebP (lossless) | ✓ | — | ~25% smaller than PNG |
| WebP (lossy) | — | ✓ | ~30% smaller than JPEG |
Android support:
- WebP lossless/alpha: API 18+
- WebP lossy: API 14+
Convert in Android Studio:
Right-click any PNG/JPEG → Convert to WebP... → choose quality (80–90 is usually indistinguishable from original).
Convert via CLI:
# Install: brew install webp
cwebp -q 80 input.png -o output.webp
Vector Drawables: Scalable Icons
Use VectorDrawable for icons, illustrations, and any graphic that needs to render at multiple densities. One .xml file instead of 5 PNG densities (mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi):
<!-- ic_arrow.xml — scales perfectly at any density -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/on_surface"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
</vector>
Using in code:
imageView.setImageDrawable(
AppCompatResources.getDrawable(context, R.drawable.ic_arrow)
)
// Or in XML:
// android:src="@drawable/ic_arrow"
Animated Vector Drawables
Replace GIFs with AnimatedVectorDrawable for small looping animations:
<!-- res/drawable/loading_animation.xml -->
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_loading">
<target
android:name="rotation_group"
android:animation="@animator/rotate_360" />
</animated-vector>
val animatable = AnimatedVectorDrawableCompat.create(context, R.drawable.loading_animation)
imageView.setImageDrawable(animatable)
animatable?.start()
Dynamic Image Sizing via URL Parameters
When serving images from a CDN, request only the resolution you need:
fun ImageView.loadOptimized(imageUrl: String) {
// Request image at 2× display size (for high-density screens)
val widthPx = width.takeIf { it > 0 } ?: 400
val heightPx = height.takeIf { it > 0 } ?: 300
val density = resources.displayMetrics.density.toInt()
val optimizedUrl = "$imageUrl?w=${widthPx * density}&h=${heightPx * density}&fmt=webp&q=80"
load(optimizedUrl) {
placeholder(R.drawable.placeholder)
crossfade(true)
}
}
APK Image Asset Checklist
| Rule | Reasoning |
|---|---|
| Convert all PNGs/JPEGs ≥ 10 KB to WebP | 25–30% APK size reduction |
| Use vectors for icons < 200×200dp | One file vs 5 density buckets |
Use 9-patch (.9.png) for scalable stretchable images | Avoids large background bitmaps |
Check with aapt dump badging for density coverage | Verify all required densities are present |
Profile with resconfigs to strip unused densities | Removes densities not in your supported set |
// build.gradle.kts — strip unused languages and densities (reduces APK)
android {
defaultConfig {
resourceConfigurations += setOf("en", "xxhdpi", "xxxhdpi")
}
}
Key Takeaways
- WebP lossless saves ~25% vs PNG; lossy saves ~30% vs JPEG with comparable quality
VectorDrawablereplaces 5 density PNG files with one XML — use for all icons and simple illustrations- Request CDN images at display size × density, in WebP format
AnimatedVectorDrawableis the GIF replacement for small looping animations