Enable R8 optimizations for release builds - #16308
Conversation
db1109e to
00e0561
Compare
|
|
There was a problem hiding this comment.
AI Code Review - Found 2 potential issues
This PR enables R8 full mode, R8 optimizations (-dontoptimize removed), and shrinkResources, and narrows several broad -keep rules. These are release-only changes, so nothing here shows up in a normal debug run; the risk is entirely in what optimization/shrinking removes from a release build. The changes are well-scoped and clearly commented, but the two spots below are the ones most likely to bite silently.
PR housekeeping
- FYI (not a blocker): this only affects release/minified builds, so verification should be done against a shrunk
release(orwasabiRelease) build, exercising reflection-heavy paths (Gson deserialization of API responses, Zendesk support flow, EventBus events, and country-name lookup) rather than a debug build. - Applied repo
AGENTS.md/CLAUDE.mdguidance while reviewing.
Automatic review · claude-opus-4-8 · Workflow run
How to reply to a finding
Reply on this review (or inline at the line the finding refers to) with one of:
@claude addressed- I made the change. Bot verifies against the next diff before marking resolved.@claude rejected: <reason>- Will not fix; reason gets quoted on the next review.@claude not-applicable- Finding does not apply (wrong file, already covered elsewhere, etc.).
The bot honours these on the next review pass.
| // R8 shrinks and optimizes the app (dead code removal, inlining, class merging), | ||
| // but we don't obfuscate the bytecode (-dontobfuscate in proguard-rules.pro). | ||
| minifyEnabled = true | ||
| shrinkResources = true |
There was a problem hiding this comment.
AI Code Review [fix here]
Issue: shrinkResources = true strips resources that are only referenced by a dynamically-built name. StringUtils.getCountryByCountryCode() resolves country names via resources.getIdentifier("country_mapping_$countryCode", "string", ...), so the country_mapping_* strings are never referenced statically. Although the shrinker's safe mode tries to detect getIdentifier usage, the lookup name is built by interpolation (only the country_mapping_ prefix is a real constant), so these strings can be removed in a release build, silently breaking country-name resolution (it falls back to null).
Suggestion: Verify on a shrunk release build that country names still resolve, and add a res/raw/keep.xml with tools:keep="@string/country_mapping_*" (plus any other resource resolved via getIdentifier) so they are guaranteed to survive shrinking.
There was a problem hiding this comment.
@claude not-applicable — the country_mapping_* strings don't exist in the unshrunk baseline APK either: all 241 were deleted by 5141ca4 ("Remove unused strings", Sept 2025), which static analysis drove precisely because the only reference is the dynamic getIdentifier lookup. getCountryByCountryCode() has returned null in production ever since, so resource shrinking changes nothing here. Pre-existing issue tracked in WOOMOB-3701.
| ##### WooCommerce - begin | ||
| # Gson populates fields reflectively, so keep fields and enum constants of our classes | ||
| # (we don't obfuscate, so names are stable). Methods stay eligible for R8 optimization. | ||
| -keepclassmembers class com.woocommerce.** { <fields>; } |
There was a problem hiding this comment.
AI Code Review [fix here]
Issue: Replacing -keep class com.woocommerce.** { *; } (and the FluxC equivalent below) with -keepclassmembers ... { <fields>; } is riskier under R8 full mode than it looks: -keepclassmembers preserves members only if the class itself is kept. A Gson model reached purely by reflection (e.g. the element type of a generic collection, or a polymorphic subtype) with no static reference in retained code can be shrunk away entirely under full mode, in which case keeping its fields does nothing and deserialization returns null/crashes.
Suggestion: Confirm on a release build that every API response model still deserializes (orders, products, payments, settings, notifications, etc.). For any model that is only constructed reflectively, keep the class itself (-keep class ... { <fields>; } or @Keep) rather than only its members.
There was a problem hiding this comment.
@claude rejected: verified empirically on the shrunk wasabiRelease mapping — the Gson models from all app-module call sites survive, including the worst-case pattern described here: SitePlanRestClient$SitePlanDto is reached only via TypeToken<Map<Int, SitePlanDto>>, yet is retained because the consuming code reads its properties (it.currentPlan). That's the general mechanism: any deserialized model whose data is actually consumed is statically referenced by the consuming code (property reads, checkcasts, signatures), so R8 keeps the class; a model with zero static references couldn't be used by the app at all. The release-notes entry carries the [*****] flag, so full smoke tests on the final APK cover the residual tail.
|
@malinajirka ack - but can this wait for a few days? I wanted to take a deep look but I just have some other things to finish. |
|
@wzieba Sure, no rush at all - feel free to look into it whenever. It's not blocking anything, it's just an improvement. |
|
Version |
wzieba
left a comment
There was a problem hiding this comment.
Thanks @malinajirka for working on this! I've left a few comments. None of them are blocking, this PR is a good iteration towards fixing our R8 config.
|
|
||
| android.nonTransitiveRClass=true | ||
| android.enableR8.fullMode=false | ||
| android.enableR8.fullMode=true |
There was a problem hiding this comment.
Nice, this had to be done before ~end of the year anyway 👏
WARNING: The option setting 'android.enableR8.fullMode=false' is deprecated.
[task 2026-08-17T11:17:30.901+00:00] The current default is 'true'.
[task 2026-08-17T11:17:30.901+00:00] It will be removed in version 10.0 of the Android Gradle plugin.
| -keepclassmembers class org.wordpress.android.fluxc.** { <fields>; } | ||
| -keepclassmembers enum org.wordpress.android.fluxc.** { *; } |
There was a problem hiding this comment.
FluxC (Gson deserialization; model/network field keeps come from fluxc's consumer-rules.pro) - begin
True - can we move this to FluxC's consumer-rules.pro then?
And generally, these rules are still pretty broad, but maybe it's a good iteration.
| ###### OkHttp (the library ships its own consumer rules) - begin | ||
| -dontwarn okio.** | ||
| -dontwarn okhttp3.** | ||
| -keep class okhttp3.** { *; } | ||
| -keep interface okhttp3.** { *; } | ||
| -dontwarn com.squareup.okhttp.** | ||
| -keep class com.squareup.okhttp.** { *; } | ||
| -keep interface com.squareup.okhttp.** { *; } | ||
|
|
||
| -keepattributes Signature | ||
| -keepattributes *Annotation* | ||
| ###### OkHttp - end |
There was a problem hiding this comment.
OkHttp (the library ships its own consumer rules) - begin
That's 100% true: can we remove all rules then?
| ###### OkHttp (the library ships its own consumer rules) - begin | |
| -dontwarn okio.** | |
| -dontwarn okhttp3.** | |
| -keep class okhttp3.** { *; } | |
| -keep interface okhttp3.** { *; } | |
| -dontwarn com.squareup.okhttp.** | |
| -keep class com.squareup.okhttp.** { *; } | |
| -keep interface com.squareup.okhttp.** { *; } | |
| -keepattributes Signature | |
| -keepattributes *Annotation* | |
| ###### OkHttp - end |
| # Only required if you use AsyncExecutor | ||
| -keepclassmembers class * extends de.greenrobot.event.util.ThrowableFailureEvent { | ||
| ** *(java.lang.Throwable); | ||
| } |
| -keepclassmembers class ** { | ||
| public void onEvent*(**); | ||
| } |
There was a problem hiding this comment.
AFAIU we don't use Event Bus 2 - if these lines are required, can we move them to Event Bus 3 section?
| ###### FluxC - end | ||
|
|
||
| ###### FluxC - WellSql (needed for Addon support) - begin | ||
| -keep class com.wellsql** { *; } |
There was a problem hiding this comment.
Normally I'd suggest it too to move to FluxC consumer-rules.pro, but WellSql should be relatively soon removed completely, so it's not worth it.
| ###### Dagger - begin | ||
| -dontwarn com.google.errorprone.annotations.* | ||
| ###### Dagger - end |
There was a problem hiding this comment.
I think we don't really need this, do we?
| ###### Dagger - end |
| ###### Google Crypto Tink dependencies - begin | ||
| -dontwarn com.google.api.client.http.GenericUrl | ||
| -dontwarn com.google.api.client.http.HttpHeaders | ||
| -dontwarn com.google.api.client.http.HttpRequest | ||
| -dontwarn com.google.api.client.http.HttpRequestFactory | ||
| -dontwarn com.google.api.client.http.HttpResponse | ||
| -dontwarn com.google.api.client.http.HttpTransport | ||
| -dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder | ||
| -dontwarn com.google.api.client.http.javanet.NetHttpTransport | ||
| -dontwarn org.joda.time.Instant | ||
| ###### Google Crypto Tink dependencies - end |
There was a problem hiding this comment.
I think we no longer use Crypto Tink, do we? At least as a direct dependency. Have you experimented with removing these -dontwarn?
| # This is generated automatically by the Android Gradle plugin. | ||
| -dontwarn java.beans.ConstructorProperties | ||
| -dontwarn java.beans.Transient | ||
| -dontwarn org.slf4j.impl.StaticLoggerBinder | ||
| -dontwarn org.slf4j.impl.StaticMDCBinder |
There was a problem hiding this comment.
As comments says, this was auto-generated by old AGP update - can we check if they're actually still required?
|
@malinajirka I just realised: if the goal of the PR was to only enable R8 optimizations, I think it's good to go. I reviewed it from "fix the R8 config" angle, which I see could be too broad and not actually the intention of this PR. |
|
Thanks for the review @wzieba!
Yeah, I essentially tried to keep the changes to minimum, except of obviously wrong setup. However, I like all your suggestions and I think all of them will work - I recorded them under https://linear.app/a8c/issue/WOOMOB-3903/follow-up-proguard-rule-cleanups-suggested-in-r8-pr-review. |
|
Version |


Fixes #WOOMOB-3699
Description
Release builds have been running R8 in shrink-only mode:
-dontoptimize, R8 compat mode (android.enableR8.fullMode=false), no resource shrinking, and several broad keep-alls (com.woocommerce.**, fluxc, OkHttp, Gson, Zendesk, Guava) that exempted most of the app from shrinking.This PR (one logical change per commit):
com.woocommerce.**and fluxc keep-alls tokeepclassmembersfor fields/enums — all Gson needs, since we keep-dontobfuscate(names never change; only removal is a risk)Result on
vanillaRelease: APK 139.4 → 127.0 MB, dex 97.5 → 60.6 MB (−38%), and R8 can now optimize (inline/merge) across the app.Notes for reviewers:
com.woocommerce.** {<fields>; }, but AFAICT the gain would be really small and we'd risk runtime failures across many features.Test Steps
Use a release build (e.g.
wasabiRelease) with a logged-in store:Images/gif
N/A
RELEASE-NOTES.txtif necessary. Use the "[Internal]" label for non-user-facing changes.