
Play Console said our DEX was 39% optimized. The culprit was a keep rule for an SDK we never used
A leftover Kakao SDK ProGuard file kept every public member of every class out of R8; deleting it moved the bundle's r8.json numbers from 39% to 85%.
Play Console still rated the latest bundle of SugarSpeller, our Korean spelling checker, as "intermediate" on September 25, 2026: optimization 36%, obfuscation 39%, shrinking 39%. Five days earlier we had removed the -dontoptimize line that kept R8 from optimizing at all. The rest of the gap came from one seven-line ProGuard file for the Kakao SDK, which the app does not use. With it gone the same day, the unshrunk share in the bundle's r8.json fell from 61.47% to 15.18% (85% once subtracted from 100), and the AAB went from 18.4 MB to 11.6 MB.
Numbers come from SugarMount's app repository (sugarspeller): commits a597223 (2026-09-20) and e05f8e3 (2026-09-25), the changelog entries for 1.6.020 and 1.6.023, the post-release watch log, and an r8.json read again from a local release AAB while writing this note. Checked on October 20, 2026.
Contents (7)
The Play Console figure is r8.json turned upside down
Those percentages line up with a file the Android Gradle Plugin (AGP) already puts in every R8-processed release bundle: BUNDLE-METADATA/com.android.tools/r8.json. Its stats block records the share of code that was not shrunk, obfuscated or optimized.
unzip -p app-release.aab BUNDLE-METADATA/com.android.tools/r8.json
"stats":{"noObfuscationPercentage":61.48,
"noOptimizationPercentage":64.27,
"noShrinkingPercentage":61.47}
Subtract each value from 100 and you get what Play shows: 61.47 became 39% shrunk, 64.27 became 36% optimized. We matched that from our own before-and-after numbers, not from a Google spec, but it held for the two Play readings we have (0% before, 36/39/39% after the first fix). That lets you read the metric locally before uploading anything.
The build used AGP 9.4.0 with R8 full mode, resource shrinking and the stock proguard-android-optimize.txt. Our repository notes say Play wants this metric addressed by February 2027; we could not re-check the wording on the Play screen for this note.
Dropping -dontoptimize took optimization from 0% to 36%
Commit a597223 on September 20 removed -dontoptimize from proguard-common.pro. It had been added on purpose, out of fear that optimization would break the Gson reflection that parses spelling results, and it pinned Play's optimization figure at 0%. The comparison baseline was the previous HEAD (1.6.019) built in the same environment.
Unoptimized code went from 100% to 64.25%, and uncompressed DEX from 16.31 MB to 14.84 MB. The AAB dropped from 29.95 MB to 18.33 MB, but most of that was a greeting-card editor and roughly 11 MB of its assets leaving in the same release. Optimization's own share sits inside the 1.5 MB of DEX.
Unobfuscated code rose from 54.16% to 61.46%. The commit put that down to "app code shrank, so the SDK keep rules weigh more" and moved on. Version 1.6.020 shipped on September 22. Three days later came the 36/39/39% reading from the top of this note.
The merged configuration.txt had a rule on class *
Every rule R8 actually applied ends up in one file, app/build/outputs/mapping/release/configuration.txt: the defaults, the app's six .pro files, and the consumer rules each library AAR brings along. The merged file from the October 9, 2026 release build is 1,369 lines, short enough to read end to end. Reading through it on September 25 turned up this block.
# proguard-kakao.pro (in the repo since the first commit, 2020-12-16)
-keep class com.kakao.** { *; }
-keepattributes Signature
-keepclassmembers class * {
public static <fields>;
public *;
}
-dontwarn android.support.v4.**,org.slf4j.**,com.google.android.gms.**
In the third rule, class * has no condition, so it matches every class in the app and in every library. Their public fields and methods can then not be removed, renamed or inlined. And the Kakao SDK (the SDK for KakaoTalk login and sharing) was not a dependency at all; even its Maven repository line in the root build script was commented out.
Commit e05f8e3 deleted the file and both build references to it, release and debug. It also deleted proguard-retrofit2.pro, which no build had ever referenced.
Do not ship an unconditional keep rule on class * in an Android app. Libraries that need rules already ship them as consumer rules inside their AAR.
Before and after: r8.json and sizes for both changes
Each pair of columns comes from a different day and a different commit, so compare within a pair. Even the same configuration drifts in the second decimal: 64.25% on September 20, 64.27% on September 25.
| Metric | 1.6.019 baseline | -dontoptimize removed (Sep 20) | Before kakao file removal (Sep 25) | After (Sep 25) |
|---|---|---|---|---|
| Not optimized | 100% | 64.25% | 64.27% | 15.68% |
| Not obfuscated | 54.16% | 61.46% | 61.48% | 15.14% |
| Not shrunk | not recorded | not recorded | 61.47% | 15.18% |
| As Play shows it (opt/obf/shrink) | optimization 0% (Play screen) | 36/39/- (derived from r8.json) | 36/39/39% (Play screen) | 84/85/85 (derived from r8.json, not yet seen on Play) |
| DEX | 16.31 MB (uncompressed) | 14.84 MB | 15.6 MB (total) | 8.2 MB |
| AAB | 29.95 MB | 18.33 MB (asset removal included) | 18.4 MB | 11.6 MB |

Measured by DEX size, deleting one rule did about five times what removing -dontoptimize did. DEX shrank by 1.5 MB on September 20 and by 7.4 MB (47%) on September 25, and the AAB lost another 6.8 MB.

Before release, a release APK signed with the debug key ran on an API 34 emulator. A spelling check against Naver (Korea's largest web portal) came back with the two corrections the server reported, and the post-check quiz bubble, the quiz screen and Remote Config content injection all worked. The five JSON key strings Gson reads were still present verbatim in the release DEX. (A headless emulator returns black screenshots, so we read screen text with uiautomator dump instead.)
From September 22 to October 3 the post-release watch log recorded zero crashes for 1.6.020, for 1.6.024 (the first release without the kakao rule) and for 1.6.025, and closed the item as "no regression".
The clue the agent explained away
An AI coding agent (Claude) wrote both commits. On September 20 it declared in the commit message that the -dontoptimize line was the cause. It also recorded, accurately, that unobfuscated code had gone up by more than 7 points. It did not check which rule was holding that code; it filled the gap with a guess that the keep rules had not changed and the SDKs simply weighed more.
On the afternoon of September 25 the operator pasted a Play Console screenshot, noted that the bundle still looked unoptimized, and asked for a deeper analysis. Only then did the agent open the merged configuration, and the seven-line file from 2020 surfaced. Opening configuration.txt first instead of guessing would have saved five days.
Two lines went into the repository's agent instructions afterwards: never add a keep rule on class *, and check the merged result in configuration.txt. The checks we run to catch release-only R8 failures before shipping are in What to check before shipping an AI-built Android app.
Still open
- Confirming the Play screen. The repository holds only the 85% derived from r8.json; nothing records the Play Console figure after release. We will capture it on the next upload.
- The ratio creeping back. A new Firebase dependency in 1.6.025 pulled the build down about 5 points, to roughly 80%. The release AAB left on disk from October 9, 2026 reads 20.23% unoptimized with 9.30 MB of DEX (9,302,488 bytes). Next step: diff configuration.txt to see which consumer rule arrived.
FAQ
Does the Play Console DEX optimization warning block uploads?
It did not in September 2026: bundles at 0% and at 36% optimization both shipped normally. Our repository notes say Play expects it fixed by February 2027, so it is not something to leave for later.
Can I see the optimization percentage before uploading to Play?
Yes. Extract BUNDLE-METADATA/com.android.tools/r8.json from the release AAB and subtract the three stats values from 100. For this app, 61.47 matched Play's 39%. That mapping is our inference from repository records, and nothing yet records whether Play showed the 85% that 15.18 implies.
How do I find the keep rule that is holding the metric down?
Search the merged configuration.txt for -keep or -keepclassmembers rules on wide patterns such as class * or ** with no condition attached. Here it was a single -keepclassmembers class * { public *; } block in a file for an SDK the app did not use.
This note is written from values SugarMount measured itself; an AI coding agent was used to draft it.
More AX notes
- Pre-gzipping Android assets, measured: zero APK savings, and .pack.gz became .pack with a FileNotFoundException
- A market:// link opens a store chooser on SKT-sold Samsung phones: set the Play package first
- A splash that never moved on: addOnCompleteListener(this) and onStop
- "6.7" < "6.11" is false: comparing Remote Config versions as strings froze content updates
- A reverse-construction generator and the gate behind 1,100 arrow-puzzle levels
- Daily users rose after the redesign while organic D1 retention fell from 15.2% to 11.2%