AppMama

blog / performance · gradle

How to Reduce Android App Size (APK and AAB)

· 9 min read · AppMama

To reduce Android app size, turn on R8 with resource shrinking, ship an Android App Bundle instead of a universal APK, and then measure with APK Analyzer to find what is actually big. Those three steps usually cut a typical app by a third or more; everything after that is targeted work on images, native libraries and dependencies.

This guide goes through each step in order of payoff, with the Gradle Kotlin DSL for current Android Gradle Plugin (AGP) versions.

How do I see what is making my APK big?

Measure before you change anything, or you will spend an afternoon converting PNGs that were 2% of the file. In Android Studio, choose Build → Analyze APK… and open a release APK or AAB (or drag the file onto the editor). APK Analyzer shows every entry with its raw size and its download size, which is the compressed size users actually pay for.

What to look at, in order:

APK Analyzer also has a Compare with previous APK button. Use it on every release to catch the dependency bump that quietly added 3 MB.

For CI, the same data is available from the apkanalyzer command-line tool in the SDK's cmdline-tools:

Terminal

apkanalyzer -h apk file-size app-release.apk
apkanalyzer -h apk download-size app-release.apk

Always measure a release build. Debug builds skip R8 and contain debug metadata, so their size tells you very little.

How do I enable R8 and resource shrinking?

R8 is the optimizer built into AGP. It removes unreachable code (tree shaking), inlines and merges classes, and shortens names. Resource shrinking then removes resources that nothing reachable references. Together they are the biggest single win for most apps, especially Compose apps, which carry a lot of library code that R8 removes.

The DSL depends on your AGP version. On AGP versions before 9.3, and still supported on newer ones, use the familiar flags:

app/build.gradle.kts (AGP before 9.3)

android {
    buildTypes {
        release {
            // Code shrinking, optimization and obfuscation (R8)
            isMinifyEnabled = true
            // Remove unused resources (requires isMinifyEnabled)
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

AGP 9.3 added a simpler optimization block that turns on code and resource optimization together and includes the default Android keep rules:

app/build.gradle.kts (AGP 9.3 and newer)

android {
    buildTypes {
        release {
            optimization {
                enable = true // code and resource optimization
            }
        }
    }
}

With the new block, your own keep rules go in files ending in .keep under src/<variant>/keepRules/, for example src/main/keepRules/app.keep. Check the enable app optimization page for the current DSL, because this area has changed several times recently.

Version details worth knowing:

Turn on R8 and resource shrinking before anything else in this guide. It is two lines, it often removes more than every other technique combined, and it makes the app faster too.

What if R8 breaks my release build?

R8 can only see code that is reachable through normal calls. Anything loaded by reflection, such as serializer classes, JNI callbacks or classes named in strings, may be removed or renamed. Symptoms are a ClassNotFoundException or a JSON field that silently comes back null in release only.

The fix is a targeted keep rule, not -dontobfuscate or -keep class com.example.** { *; }. Broad rules throw away most of what R8 gives you.

app/proguard-rules.pro

# Keep a class loaded by name through reflection
-keep class com.example.app.plugins.ExportPlugin { <init>(); }

# Keep fields of a model parsed with a reflection-based JSON library
-keepclassmembers class com.example.app.api.UserDto { <fields>; }

Most modern libraries ship their own consumer rules, and libraries built on code generation (kotlinx.serialization, Moshi codegen, Room) need few or none. Prefer those over reflection-heavy alternatives. Upload the generated mapping.txt to Play Console so crash stack traces are deobfuscated. If R8 reports missing classes during the build, see fixing common Android Gradle build errors.

If resource shrinking removes something you load dynamically, for example with getIdentifier(), keep it explicitly in a file such as res/raw/keep_example.xml:

res/raw/keep_example.xml

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@drawable/flag_*,@raw/onboarding_video"
    tools:discard="@layout/debug_overlay" />

Android App Bundle vs APK: how much does it save?

An APK you build locally contains everything for every device: all ABIs, all screen densities, all languages. An Android App Bundle (.aab) is an upload format; Google Play generates split APKs for each device, so a phone downloads only its own ABI, its own density and the languages configured on the device.

New apps on Google Play must use App Bundles, so if you publish there you already get this. The common mistake is measuring the wrong thing: the size of the .aab file is not what users download. To see real per-device numbers locally, use bundletool:

Terminal

./gradlew :app:bundleRelease

bundletool build-apks \
  --bundle=app/build/outputs/bundle/release/app-release.aab \
  --output=app.apks

bundletool get-size total --apks=app.apks

get-size total prints the minimum and maximum download size across device configurations. Play Console shows the same information under App size for each release, along with size-saving recommendations.

Google Play caps the compressed download of an App Bundle app at 200 MB (100 MB for apps still uploaded as APKs). Play Asset Delivery and Play Feature Delivery let you go beyond that, but being near the limit is a sign something else in this guide needs doing first.

If you distribute APKs outside Play (GitHub releases, F-Droid, enterprise), you lose this per-device splitting. In that case consider building per-ABI APKs with splits { abi { … } } rather than one universal APK.

How do I make images smaller?

Images are the most common source of bloat in res/. In order of payoff:

  1. Use vector drawables for icons and simple illustrations. A single VectorDrawable of a few hundred bytes replaces five density buckets of PNGs. Material Symbols are available as vectors through Android Studio's Vector Asset tool. Keep vectors for small, simple images; very large or complex vectors are slow to render.
  2. Convert remaining bitmaps to WebP. Right-click a drawable folder in Android Studio and choose Convert to WebP…. Lossy WebP at quality 75–85 is visually identical to most JPEGs at a fraction of the size, and lossless WebP is generally smaller than PNG. Check the preview for images with sharp edges or text.
  3. Do not ship every density. If you only have one high-quality bitmap, put a single xxhdpi version in drawable-xxhdpi/ and let Android scale it, rather than generating mdpi through xxxhdpi. With App Bundles, unused densities are not downloaded anyway, so this matters most for sideloaded APKs.
  4. Replace frame-by-frame animations. An AnimationDrawable with 30 PNG frames is huge. Use an animated vector drawable, or a Lottie file if the animation genuinely needs it.
  5. Move large, optional images to the network. Onboarding photos or seasonal artwork can be loaded with an image library like Coil and cached, instead of bundled.

Also check assets/. Fonts are a frequent surprise: a full CJK font can be 10–20 MB. Use downloadable fonts or subset the font to the characters you need.

How do I remove unused languages from libraries?

AndroidX, Material and Play services ship translations for dozens of languages. If your app supports three, the rest are dead weight in resources.arsc, and in a universal APK every user downloads them. Tell AGP which locales to keep with localeFilters (added in AGP 8.8; older projects used resourceConfigurations in defaultConfig):

app/build.gradle.kts

android {
    androidResources {
        // Keep resources only for the languages the app supports
        localeFilters += listOf("en", "fr", "de")
    }
}

Only list languages you have actually translated your own strings into. Otherwise a German user sees your English UI next to German system dialogs from a library, which looks broken. App Bundles already deliver only the device's languages, so this matters most for APK distribution and for keeping resources.arsc lean.

How do I shrink native libraries?

If lib/ dominates APK Analyzer, you have native code, either your own (NDK, CMake) or from a dependency such as a database engine, video codec, ML runtime or game engine.

app/build.gradle.kts

android {
    packaging {
        jniLibs {
            // Store .so files uncompressed; do not extract them on install
            useLegacyPackaging = false
        }
    }
}

The download is slightly larger with uncompressed libraries, because Play compresses them for transfer anyway, but on-device install size is smaller. That is usually the better trade.

Which dependencies are making my app bigger?

R8 removes unused code from libraries, but it cannot remove code that is reachable, and some libraries make a lot reachable. To find the offenders:

Common, cheap wins are replacing Guava with the Kotlin standard library, using the platform ImageDecoder for animated GIF and WebP instead of a GIF library, and choosing protobuf-kotlin-lite over full protobuf. Large SDKs (analytics, ads, maps) often have modular artifacts; depend on the module you use, not the umbrella artifact. For a list of libraries that tend to stay lean, see essential Android libraries for a new app, and keep versions tidy with Gradle version catalogs so duplicates are easy to spot.

A release-day app size checklist

StepTypical savingEffort
R8 + resource shrinking on the release buildLarge (often 20–50% of DEX and res)Low, plus keep-rule fixes
Publish as an App BundleLarge for apps with native code or many languagesLow
Vectors and WebP instead of PNG/JPEGMedium, depends on image countLow to medium
localeFilters for supported languagesSmall to mediumLow
Trim ABIs / split APKs (non-Play)Large when native libs existLow
Replace heavy dependenciesVaries widelyMedium to high

Add APK Analyzer's compare view or bundletool get-size total to your release routine. App size rarely jumps in one change; it creeps up one dependency at a time.

Shrinking also interacts with signing and publishing: Play generates and signs the split APKs from your bundle with your app signing key. If that setup is unfamiliar, read Android app signing explained.

Frequently asked questions

Does isMinifyEnabled make my app slower to build?

Yes, R8 adds time to release builds, sometimes a minute or more on large apps. That is why it is normally enabled only for the release build type. Debug builds stay fast and unshrunk.

Why is my AAB file bigger than my APK?

The App Bundle contains code and resources for every device configuration, so the file you upload is large. Users never download it directly; Google Play builds smaller split APKs for each device. Use bundletool get-size total or the App size page in Play Console to see real download sizes.

Will R8 obfuscation break crash reports?

Stack traces from a release build show shortened class and method names. Upload the mapping.txt file generated for each release to Play Console or your crash reporting tool, and they will be translated back automatically.

Is WebP supported on all Android versions?

Lossy WebP works on every Android version a modern app targets. Lossless and transparent WebP need Android 4.3 (API 18) or newer, which is below the minimum SDK of current AndroidX libraries, so in practice you can use WebP everywhere.

Should I remove x86 libraries from my app?

If you publish an App Bundle, keep them; each device downloads only its own ABI, so they cost nothing for phone users. If you ship a universal APK, dropping x86 and x86_64 saves space but stops the app running on x86 emulators and the rare x86 device.