CORE JSC

International Technology Partnership

Mobile Development

Fixing OutOfMemoryError During R8/ProGuard Minification in React Native Android Release Builds

Release builds worked for months, but once the app grows past a certain size, assembleRelease starts failing with an OutOfMemoryError during R8/ProGuard minification — often only on CI. Here's the real cause and the definitive fix.

Core JSC Team·July 28, 2026
React NativeAndroidGradleR8ProGuardBuildCI/CD

The Problem

A React Native app's debug build compiles fine, and release builds worked for months — until the app grows past a certain size (more native modules, more JS bundled code, more third-party SDKs) and suddenly ./gradlew assembleRelease fails with java.lang.OutOfMemoryError: Java heap space during the R8/ProGuard minification step specifically, often on CI machines with less memory than a developer's laptop, or after upgrading to a version of React Native that pulls in more native dependencies.

Why It Happens

R8 processes the entire merged bytecode graph in one pass, in memory

Unlike incremental compilation, R8's shrinking, obfuscation, and optimization passes need to hold the full merged class/method graph of the app and every dependency in memory at once to correctly trace what's actually reachable. As the app accumulates more native modules and SDKs, that graph grows, and the default JVM heap Gradle allocates for the R8 worker often doesn't grow with it.

Gradle's default JVM memory settings are conservative

Gradle's own daemon and the R8 subprocess both have memory ceilings, set either by Gradle's defaults or a project's gradle.properties, that were often set once early in a project's life and never revisited as the app grew.

CI runners frequently have less RAM than local development machines

A release build that passes locally can still fail in CI if the CI runner has less available memory (common on free-tier or budget-constrained CI plans), which is why this often surfaces first as "it works on my machine" during a release pipeline.

The Fix

1. Raise Gradle's and R8's JVM heap explicitly

# android/gradle.properties
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
android.enableR8.fullMode=true

This is the direct fix for the OOM itself. On CI, this setting needs to actually take effect for the runner — verify the CI machine has enough physical/allocated RAM to satisfy the value you set, not just that the config file says it.

2. Reduce what R8 actually has to process by trimming the dependency surface

// android/app/build.gradle
android {
  buildTypes {
    release {
      minifyEnabled true
      shrinkResources true
      proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
  }
}

Audit for native modules or SDKs pulled in but no longer used (a common source of dead weight after a library migration), and remove them — every unused dependency still adds classes R8 has to trace and decide are unreachable, which costs memory even though nothing from it ships in the final APK.

3. Split the build into more Gradle daemon-friendly chunks where possible

For very large apps, enabling android.enableSeparateAnnotationProcessing or splitting a monolithic app module into feature modules reduces how much a single R8 invocation has to process in one pass, spreading the memory cost across smaller, incremental units instead of one giant graph.

4. Confirm the CI machine's actual available memory before increasing heap size blindly

free -h

Setting -Xmx higher than the machine's physical RAM just trades an OOM crash for the OS killing the process outright (or heavy swapping that makes the build effectively hang) — check what's actually available first, and increase the CI runner's memory tier if the app's real requirement has outgrown it.

Why This Works

None of this makes R8's job smaller by magic — it either gives the process enough memory to actually hold the graph it needs, or shrinks that graph by removing dependencies that were never contributing to the shipped app. Matching the heap size to the app's real size, and keeping the dependency surface honest, is what keeps minification succeeding as the app continues to grow.

Conclusion

An OutOfMemoryError during R8/ProGuard minification is a sign the app has genuinely outgrown its build configuration's memory assumptions, not a bug in R8 itself. Raise the JVM heap explicitly in gradle.properties, trim unused dependencies that add dead weight to the graph, and confirm the CI runner actually has the memory the new setting assumes.