App Size Optimization for Android and iOS
App size optimization directly impacts user acquisition and retention. Research shows that for every 6MB increase in app size, install conversion rates drop by 1%. On Android, apps over 150MB cannot be downloaded over mobile data in many markets. On iOS, apps over 200MB require WiFi for cellular downloads. Reducing your app size means more installs, fewer uninstalls, and faster updates.
This guide covers practical strategies for both Android and iOS, from code shrinking and asset optimization to dynamic delivery and size budgets. These techniques can reduce app sizes by 40-70% without sacrificing functionality or user experience.
Android: R8 Code Shrinking
R8 is Android’s default code shrinker, optimizer, and obfuscator. It removes unused code and resources, inlines methods, and reduces the DEX file size. Moreover, R8’s full mode provides more aggressive optimizations:
// build.gradle.kts
android {
buildTypes {
release {
isMinifyEnabled = true // Enable R8
isShrinkResources = true // Remove unused resources
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
// Enable Android App Bundle (AAB)
bundle {
language {
enableSplit = true // Separate APKs per language
}
density {
enableSplit = true // Separate APKs per screen density
}
abi {
enableSplit = true // Separate APKs per CPU architecture
}
}
}# proguard-rules.pro — Keep rules for common libraries
-keepattributes *Annotation*
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile
# Retrofit
-keepattributes Signature
-keep class retrofit2.** { *; }
-keepclassmembers,allowobfuscation interface * {
@retrofit2.http.* ;
}
# Kotlin serialization
-keepattributes InnerClasses
-keep,includedescriptorclasses class
com.example.**$$serializer { *; }
-keepclassmembers class com.example.** {
*** Companion;
}
# Room database
-keep class * extends androidx.room.RoomDatabase
-keep @androidx.room.Entity class *
-dontwarn androidx.room.paging.** Android: Dynamic Feature Modules
Dynamic feature modules let you deliver features on-demand instead of including everything in the initial download. Therefore, the base APK stays small while users download features only when needed:
// In dynamic feature module's build.gradle.kts
plugins {
id("com.android.dynamic-feature")
}
android {
namespace = "com.example.feature.scanner"
}
dependencies {
implementation(project(":app"))
implementation("com.google.android.play:feature-delivery-ktx:2.1.0")
}
// Request feature installation at runtime
class FeatureManager(private val context: Context) {
private val splitInstallManager =
SplitInstallManagerFactory.create(context)
suspend fun installScanner(): Result<Unit> {
val request = SplitInstallRequest.newBuilder()
.addModule("scanner")
.build()
return suspendCancellableCoroutine { cont ->
splitInstallManager.startInstall(request)
.addOnSuccessListener {
cont.resume(Result.success(Unit))
}
.addOnFailureListener { exception ->
cont.resume(Result.failure(exception))
}
}
}
fun isInstalled(moduleName: String): Boolean {
return splitInstallManager.installedModules
.contains(moduleName)
}
}iOS: App Thinning
iOS app thinning includes slicing, bitcode, and on-demand resources. Additionally, asset catalogs automatically generate optimized variants for each device:
// Asset Catalog optimization
// Use asset catalogs instead of loose files
// Xcode automatically generates 1x, 2x, 3x variants
// On-Demand Resources (ODR) for large assets
// Tag resources in Xcode → Build Settings → On Demand Resources
class ResourceManager {
func loadLevel(named tag: String) async throws -> Bool {
let request = NSBundleResourceRequest(tags: [tag])
// Check if already downloaded
if request.conditionallyBeginAccessingResources(
completionHandler: { available in
if available {
// Resources already cached
}
})
{
return true
}
// Download on-demand
try await request.beginAccessingResources()
return true
}
func freeResources(tag: String) {
let request = NSBundleResourceRequest(tags: [tag])
request.endAccessingResources()
}
}# Analyze iOS app size
xcrun bitcode-strip MyApp -r -o MyApp_stripped
# Check app thinning report
xcodebuild -exportArchive \
-archivePath MyApp.xcarchive \
-exportPath export \
-exportOptionsPlist ExportOptions.plist \
-exportThinning UniversalAppsAsset Optimization Strategies
Images and other assets often account for 50-70% of app size. Consequently, optimizing assets provides the biggest absolute savings:
# Android: Convert PNGs to WebP (50-70% smaller)
# In Android Studio: Right-click → Convert to WebP
# iOS: Use HEIC for photos, asset catalogs for icons
# Xcode automatically compresses catalog assets
# Both platforms: SVG for vector graphics
# Android: VectorDrawable (XML)
# iOS: PDF vectors in asset catalogs
# Image compression pipeline (CI/CD)
# Install: npm install -g sharp-cli imagemin-cli
# Compress PNGs
find app/src/main/res -name "*.png" -exec \
pngquant --quality=65-80 --skip-if-larger --force \
--output {} {} \;
# Convert to WebP
find app/src/main/res -name "*.png" -exec \
cwebp -q 80 {} -o {}.webp \;Size Impact by Optimization Type
┌────────────────────────────┬──────────┬──────────┐
│ Optimization │ Android │ iOS │
├────────────────────────────┼──────────┼──────────┤
│ R8/Bitcode code shrinking │ -15-25% │ -10-15% │
│ Resource shrinking │ -5-10% │ N/A │
│ Image → WebP/HEIC │ -20-40% │ -15-30% │
│ App Bundle/Thinning │ -30-50% │ -20-30% │
│ Dynamic delivery/ODR │ -10-40% │ -10-30% │
│ Remove unused libraries │ -5-15% │ -5-15% │
│ ProGuard dictionary │ -2-5% │ N/A │
│ Native lib stripping │ -5-10% │ -3-8% │
├────────────────────────────┼──────────┼──────────┤
│ Combined (typical) │ -40-70% │ -35-60% │
└────────────────────────────┴──────────┴──────────┘Size Budget Monitoring
Prevent size regression with automated size budgets in CI:
# .github/workflows/size-check.yml
name: App Size Check
on: [pull_request]
jobs:
android-size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Build release bundle
run: ./gradlew bundleRelease
- name: Check bundle size
run: |
SIZE=$(stat -f%z app/build/outputs/bundle/release/app-release.aab 2>/dev/null || stat -c%s app/build/outputs/bundle/release/app-release.aab)
MAX_SIZE=20971520 # 20MB budget
echo "Bundle size: $((SIZE / 1024 / 1024))MB"
if [ "$SIZE" -gt "$MAX_SIZE" ]; then
echo "ERROR: Bundle exceeds 20MB budget!"
exit 1
fi
- name: Size diff report
uses: nicklegan/github-repo-size-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}// Android: Runtime size analysis
class SizeAnalyzer(private val context: Context) {
fun getAppSizeInfo(): Map<String, Long> {
val pm = context.packageManager
val appInfo = pm.getApplicationInfo(
context.packageName, 0)
val sourceDir = File(appInfo.sourceDir)
val dataDir = File(appInfo.dataDir)
return mapOf(
"apk_size" to sourceDir.length(),
"data_size" to getDirectorySize(dataDir),
"cache_size" to getDirectorySize(context.cacheDir),
"total" to (sourceDir.length() +
getDirectorySize(dataDir)),
)
}
private fun getDirectorySize(dir: File): Long {
return dir.walkTopDown()
.filter { it.isFile }
.sumOf { it.length() }
}
}When NOT to Use Aggressive Size Optimization
Over-aggressive code shrinking can break reflection-based libraries. If your app uses runtime annotation processing, serialization libraries, or dependency injection frameworks, verify R8/ProGuard rules thoroughly before release. Furthermore, dynamic delivery adds complexity to testing and QA — every module combination needs verification. For apps already under 30MB, the engineering effort of implementing dynamic features may not justify the marginal size improvement. Finally, removing rarely-used features entirely is often more effective than conditionally delivering them.
Key Takeaways
- App size optimization reduces install abandonment — every 6MB increase drops conversion by 1%
- Android App Bundles with density/language/ABI splits reduce download size by 30-50%
- Image optimization (WebP/HEIC conversion) provides the largest absolute size reduction at 20-40%
- Dynamic feature modules and On-Demand Resources defer non-essential content from the initial download
- Automated size budgets in CI prevent gradual size regression across development cycles
Related Reading
- Jetpack Compose Performance Optimization
- Android Baseline Profiles for Startup Performance
- Flutter Impeller Rendering Engine Guide