App Size Optimization: Reducing Android and iOS Bundle Sizes in Production

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.**
App size optimization Android iOS bundle reduction
Code shrinking and resource optimization dramatically reduce app download sizes

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 UniversalApps

Asset 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 \;
Mobile app asset optimization and compression
Asset compression and format conversion provide the largest size reductions
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.

Mobile app size monitoring and regression prevention
Automated size budgets prevent gradual size regression across development cycles

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

External Resources

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top