[KongchangAI]
· 3 min read· 1,700 words

Type-Safe Keep Rules in R8 and DEX Optimization in Practice

Type-Safe Keep Rules in R8 and DEX Optimization in Practice

Google Play sets DEX optimization as a hard threshold, and the R8 toolchain helps apps meet it.

Google Play now requires DEX shrinking, optimization, and obfuscation to each reach 25% or more, otherwise app visibility may be affected. The article identifies broad keep rules as the main cause of insufficient optimization, especially those from reflection and library consumer rules. Google offers three tools: the R8 configuration analyzer, Jetpack Keep, and Android CLI analyze. The analyzer visualizes rule impact, Jetpack Keep generates type-safe narrow rules from reflective code, and the CLI validates locally with the same logic as Play. Grab and Whoop cases show that combining these with baseline profiles significantly reduces ANRs and improves startup speed. The team recommends continuous governance along the "analyze–generate–validate" path.

Google Play's requirements for DEX optimization have gone from a "nice to have" to a hard threshold. With hardware supply tight and device memory constrained, a well-optimized DEX directly determines whether an app uses less memory, starts faster, and reduces ANRs. More concretely, Google Play has announced new app quality requirements under which DEX code optimization (shrinking, optimization, obfuscation) must reach 25% or more; apps that fall short may see their visibility and publishing ability on the Play Store affected. According to Brianna from the Android DevRel team and Chris Krieg from the Performance team in a technical talk, 41% of top apps are still below the 25% optimization threshold, while 13% of top apps have reached three times that threshold.

R8 configuration analyzer report

Why DEX optimization is getting renewed attention

The benefits of optimization are backed by real data. After adopting R8 full mode and refining its keep rules, Grab achieved 78% optimization, 79% shrinking, and 82% obfuscation; combined with baseline profiles, ANRs dropped by 32% and startup speed improved by 35%. When top apps enable R8 for the first time, average DEX size drops by 38% and user-perceived ANRs fall by 16%. Apps already using R8 that further raise optimization, shrinking, and obfuscation above 50% gain on average another 25% reduction in DEX size, a 4% reduction in startup time, and an additional 17% reduction in user-perceived ANRs.

These numbers show that R8 is not a "configure once and forget" option but an engineering practice that requires ongoing governance. Historically, reflective calls made inputs unpredictable and easily led to runtime crashes, so app teams and library teams tended to add broad keep rules to "stay safe." Once such rules accumulate, they snowball and erode the entire app's optimization headroom.

R8 full mode is a more aggressive optimization mode in R8. Unlike the default compatibility mode, full mode assumes all code follows standard Java/Kotlin semantics, enabling more comprehensive class merging, method inlining, and member rewriting. This can significantly increase optimization and obfuscation ratios, but it also exposes reflection or dynamic invocation issues that were previously masked by loose rules. Therefore, enabling full mode usually requires more precise keep rules and thorough testing. Grab's practice shows that once scenarios such as reflection and dependency injection are handled properly, the ANR and startup benefits of full mode are substantial.

From broad wildcards to type-safe annotations

The root of the problem is reflection. Chris used an object instantiation function as an example: creating an object via reflection by referencing a class name as a string. R8 cannot see this relationship during static analysis and can only fall back on keep rules. Ideally, reflection should be avoided as much as possible—especially now that mature code-generation tools for serialization and dependency injection exist—but optional dependencies, dynamically instantiated subclasses, serialization, and similar scenarios still make reflection hard to eliminate entirely.

In the past, writing an "ideal" keep rule was very cumbersome: you had to distinguish Kotlin classes, cover subclasses, restrict constructors, allow access modification so as not to hinder later optimization, and finally use conditional logic so the rule only takes effect when reflection is actually called. Almost no one did all of this. Hand-writing narrow rules is hard to read and maintain, while the @Keep annotation is too rigid and does not support call-site analysis.

Positioning of Jetpack Keep

Jetpack Keep's approach is to "attach" keep rules next to the reflective code. It consists of a Gradle plugin and an annotation library. At build time, it analyzes all reflection behavior within a method via lint, automatically generates annotations, and then generates keep rules from those annotations. The resulting rule targets specific subclasses, is conditional on the reflection call site, and allows subsequent optimization as much as possible—exactly the "ideal rule" from before, but no longer maintained by hand. For optional dependency scenarios where the class is not on the classpath, you can still reference the class name as a string, while parameter types use class references to ensure safety.

Jetpack Keep requires AGP 8.7 and can be applied to apps, Android libraries, AARs, KMP libraries, and jars. Its key design is that even if consumers use an older version of R8 or third-party tools, the library still exports standard consumer rules and does not introduce incompatible formats.

Keep rules (ProGuard/R8 rules) tell the optimizer which classes, methods, or fields cannot be removed or renamed. A rule typically consists of class name matching, member matching, and modifier options; for example, -keep class com.example.** { *; } keeps all members under an entire package. The broader the rule, the smaller the scope R8 can optimize; the more precise the rule, the better the optimization but the higher the maintenance cost. Dynamic access such as reflection, JNI, and serialization are the main sources of keep rules. The goal of modern tools like Jetpack Keep is to automatically generate the minimal necessary rules based on actual reflection call sites, replacing hand-written broad wildcards.

Using analysis tools to locate "bloated" rules

The R8 configuration analyzer is the starting point for investigating DEX optimization. It generates an HTML report that quantifies the three dimensions of shrinking, optimization, and obfuscation, and lists a keep-rule analysis table where you can click into each rule to see exactly how many classes, methods, and fields it retains. The report also flags subsumed rules covered by broader rules and unused rules that can be safely deleted. Note that the analyzer computes scores before final optimization and DEX compilation, so the numbers will not exactly match Play Console, but it is suitable for quickly validating optimization potential locally.

According to a case shared in the talk, the Whoop team ran the analyzer and found shrinking and obfuscation both at 74%, but optimization at 0. Further investigation revealed the build used an old ProGuard file that disabled optimization; after switching, optimization reached 55% and APK size decreased by 4%; combined with baseline profiles, cold start improved by 38% and excessive frozen frames decreased by 28%.

Brianna also introduced the R8 analyzer skill: by sending an agent a request to "analyze my R8 configuration," the tool not only generates a report but also points out the highest-impact rules. For example, it will explain that a certain package-level keep rule prevents 22% of code from being optimized and suggest narrowing that rule.

Diagnosing sources of app bloat

Baseline profiles are configuration files distributed with an app that record the list of methods on key user paths that need to be compiled ahead of time. The Android runtime uses them to perform AOT compilation during installation or background maintenance, thereby reducing interpreter and JIT overhead. They do not themselves change the DEX optimization ratio, but they complement R8's code shrinking and optimization: R8 reduces code size, while baseline profiles make the remaining critical code execute faster. In the Grab and Whoop cases, significant ANR and startup-time reductions were achieved only when baseline profiles were combined with R8.

Library keep-rule problems and CLI validation

The library ecosystem is the hardest-hit area. Many libraries ship broad rules in the name of stability or tell app developers in their documentation to add them themselves. The talk mentioned that Google's own SDKs were not immune either—more than a dozen Google libraries have fixed overly broad keep rules over the past year. A package-level wildcard rule makes all classes in an entire library be treated as entry points, so R8 cannot determine them to be dead code, and optimization headroom is severely squeezed.

The Android CLI's analyze command uses exactly the same logic as the Play Store, can analyze a bundle locally, reports DEX size and optimization percentages consistent with Play, and supports JSON output for scripted regression testing. It can also analyze AAR libraries, flagging high keep ratios, broad rules, and dangerous rules that "point to no target"—the latter often means they affect other code in an unbounded way.

Compatibility guarantees of CLI output

The team recommends this order of handling: first try upgrading the problematic library; if the problem persists, file an issue with the library developer and attach the analyze report, using data to say "you are keeping too much—please help me optimize my app." In addition, the new app template already includes package scope, AGP 9.2 makes coroutine primitives twice as fast, and Tinder saw a 13% ANR reduction after upgrading; AGP 9.3 brings a redesigned R8 API, clearer default rules, and source-directory support for keep rules.

In summary, DEX optimization is a repeatable path: use the R8 configuration analyzer or analyzer skill to find problematic rules, use Jetpack Keep to automatically generate type-safe rules from reflective code, and finally use the Android CLI to validate locally before release. Only by combining all three can you push optimization past the threshold without sacrificing stability.

AAR libraries are Android's binary library distribution format. In addition to compiled code and resources, they can carry consumer rule files such as consumer-rules.pro. These rules are automatically merged into the final R8 configuration when an app builds. If a library author writes package-level wildcard rules in consumer rules, every app using that library is forced to retain a large amount of useless code, and app developers often find it hard to notice. Therefore, analyzing the keep-rule ratio of AARs and identifying unbounded rules is an important part of governing DEX optimization.

One-line Summary

Google Play makes DEX optimization a hard threshold, and the R8 toolchain helps apps meet it.

Paragraph Summary

Google Play now requires DEX shrinking, optimization, and obfuscation to each reach 25% or more, otherwise app visibility may be affected. The article points out that broad keep rules are the main cause of insufficient optimization, especially those originating from reflection and library consumer rules. To address this, Google introduced three tools: the R8 configuration analyzer, Jetpack Keep, and Android CLI analyze. The analyzer visualizes rule impact, Jetpack Keep automatically generates type-safe narrow rules from reflective code, and the CLI validates locally using the same logic as Play. Cases such as Grab and Whoop prove that combining these with baseline profiles can significantly reduce ANRs and improve startup speed. The team recommends continuous governance along the "analyze–generate–validate" path.

#R8#DEX optimization#keep rules#Jetpack Keep#Android CLI#baseline profiles
Share:

Related articles