Reduzir Tamanho do APK e AAB no .NET MAUI: Guia Completo para Apps Menores em 2026
Trimming full, R8, Android App Bundle e divisão por ABI: as quatro alavancas que reduzem apps .NET MAUI de 90 MB para 30 MB. Guia com configuração de csproj, estudo de caso real e como validar cada corte.
Para reduzir o tamanho do APK e do AAB no .NET MAUI em 2026, combine trimming (Full mode), R8 com resource shrinker, publicação como Android App Bundle e divisão por ABI. Essas quatro alavancas, aplicadas juntas em um projeto MAUI .NET 9 típico, reduzem o instalador de download da Play Store de 60–90 MB para 25–35 MB sem sacrificar recursos.
Então, deixa eu contar como isso funciona na prática. Neste guia mostro o passo a passo que usei para levar o app fintech que mantenho de 84 MB para 31 MB (sim, mais de 50 MB cortados), incluindo pegadinhas de linker, configuração de csproj e como validar os cortes. Honestamente, alguns desses ajustes eu só aprendi depois de quebrar produção duas vezes.
Publicar como Android App Bundle (.aab) em vez de APK universal costuma cortar 30–45% do tamanho de download, porque a Play Store gera splits por ABI, densidade e idioma.
Trimming em modo full combinado com <PublishTrimmed>true</PublishTrimmed> é o maior ganho isolado, normalmente 15–25 MB em apps MAUI reais.
R8 full mode e AndroidLinkResources=true removem código Java/Kotlin e recursos Android não referenciados; sem isso, o linker do .NET não toca no lado Android nativo.
Limitar ABIs para arm64-v8a;armeabi-v7a em vez de incluir x86_64 economiza cerca de 12–18 MB no APK universal e é irrelevante para 99% dos dispositivos de usuário final.
A ferramenta APK Analyzer (ou bundletool build-apks --mode=universal) é indispensável para diagnosticar o que ainda pesa depois das otimizações.
Compilação AOT aumenta o tamanho do binário mas reduz o tempo de inicialização; é uma troca deliberada e não uma técnica de shrinking.
Por que apps .NET MAUI ficam grandes por padrão
Um projeto MAUI recém-criado com dotnet new maui, compilado em Release para Android sem nenhum ajuste, produz um APK universal na faixa de 55–75 MB. Não é bug, é o custo do modelo: cada APK carrega o runtime Mono/CoreCLR, a BCL do .NET, os assemblies do próprio MAUI, os handlers, e os binários nativos para múltiplas ABIs (arm64-v8a, armeabi-v7a e, se não desabilitado, x86_64).
Antes de tentar cortar algo, vale entender a anatomia. Extraindo um APK MAUI padrão com unzip, a distribuição costuma ser:
assemblies/: 40–55% do peso. Todos os DLLs .NET, comprimidos com LZ4.
lib/<abi>/: 25–35%. libmonosgen, libSystem.Native, libSkiaSharp e afins, replicados por ABI.
res/ e resources.arsc: 5–15%. Recursos Android (drawables, layouts nativos, strings.xml).
classes.dex: 3–8%. Bytecode Java gerado a partir dos wrappers do Xamarin.Android/Mono.
Cada linha acima corresponde a uma alavanca diferente. Trimming ataca assemblies/. R8 ataca classes.dex. Resource shrinker ataca res/. Divisão por ABI ataca lib/. Não existe uma flag mágica; a redução vem de ligar todas em conjunto e verificar cada corte.
Qual a diferença entre APK e AAB no .NET MAUI?
APK (Android Package) é o formato instalável final: um zip com tudo que roda no dispositivo. AAB (Android App Bundle) é o formato de publicação: um contêiner com todos os splits possíveis, entregue à Google Play, que gera APKs sob demanda para cada dispositivo específico. Do ponto de vista do usuário, o AAB reduz o download em 30–45% porque ele só recebe a ABI, a densidade de tela e os idiomas que o aparelho dele usa.
Na prática, em um projeto MAUI de produção quase sempre você mantém os dois: AAB para a Play Store e um APK universal assinado para testes internos com Firebase App Distribution ou download direto. O que importa não é escolher, é otimizar os dois em paralelo. Para orientação oficial da própria Google sobre o formato, veja a documentação de Android App Bundle.
Como reduzir o tamanho do APK no .NET MAUI passo a passo
Este é o conjunto mínimo de propriedades que ligo em qualquer projeto MAUI que ainda não passou por otimização. Coloco tudo dentro de um PropertyGroup condicional para Release Android no csproj, para não afetar builds de debug (que ficariam intoleravelmente lentos).
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)' == 'Release|net9.0-android'">
<!-- Trimming e linker do .NET -->
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
<!-- Compressão de assemblies -->
<AndroidEnableAssemblyCompression>true</AndroidEnableAssemblyCompression>
<AndroidStripILAfterAOT>true</AndroidStripILAfterAOT>
<!-- R8 e recursos Android -->
<AndroidLinkTool>r8</AndroidLinkTool>
<AndroidLinkResources>true</AndroidLinkResources>
<AndroidR8IgnoreWarnings>false</AndroidR8IgnoreWarnings>
<!-- ABIs suportadas (dropar x86_64) -->
<RuntimeIdentifiers>android-arm64;android-arm</RuntimeIdentifiers>
<!-- AOT: opcional, melhora startup mas aumenta tamanho -->
<RunAOTCompilation>true</RunAOTCompilation>
<AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
<!-- Formato de saída -->
<AndroidPackageFormat>aab</AndroidPackageFormat>
</PropertyGroup>
Depois de aplicar esse bloco, gere o AAB assinado com um único comando:
No primeiro build você provavelmente vai ver dezenas de avisos IL2026, IL2104, ILLink. Não ignore. Cada aviso é código sensível a reflection que o trimmer pode remover erradamente. Vou tratá-los na seção sobre erros comuns.
Trimming e configuração do linker
Trimming remove IL não referenciado no fecho estático das dependências. No modo full, o ILLink parte do Main e mantém apenas o que é alcançável por chamada direta. Tudo que só é acessado via reflection (por exemplo, Type.GetType("MyNamespace.MyClass")) é considerado morto e cortado.
Isso é ótimo para o tamanho, mas explica por que serialização, injeção de dependência e bindings de UI podem quebrar em produção quando ficam ok em debug. Três configurações que sempre uso para conter o dano:
1. Preservar assemblies inteiros que fazem reflection pesada
Crie um arquivo Properties/LinkerConfig.xml e referencie-o no csproj com <TrimmerRootDescriptor Include="Properties/LinkerConfig.xml" />:
preserve="all" é um martelo. Prefira preservar tipos ou membros específicos quando possível, porque cada assembly totalmente preservado costuma adicionar 200–800 KB.
2. Anotar tipos com DynamicallyAccessedMembers
Se você controla o código que faz reflection, é infinitamente melhor migrar para System.Text.Json com JsonSerializerContext e source generators. Isso elimina o warning na raiz e mantém o trimming agressivo. Para JSON, o padrão que uso em código novo:
[JsonSerializable(typeof(TransactionDto))]
[JsonSerializable(typeof(AccountDto))]
public partial class AppJsonContext : JsonSerializerContext { }
// uso
var options = new JsonSerializerOptions { TypeInfoResolver = AppJsonContext.Default };
var dto = JsonSerializer.Deserialize<TransactionDto>(payload, options);
3. Rodar smoke tests em Release trimmed
Isto salva a produção, de verdade. Configure a pipeline para instalar o AAB gerado em um emulador (ou dispositivo físico via ADB) e rodar um Appium/Maestro flow que cubra os caminhos com reflection. É o mesmo princípio da minha estratégia de testes em .NET MAUI: se o trimmer quebrou a serialização, é bem melhor descobrir no CI do que no Play Console. A documentação da Microsoft sobre trim self-contained deployments tem uma lista útil de padrões incompatíveis com trimming, e vale ler antes de subir para produção.
R8 e resource shrinker no Android
O trimmer do .NET só cuida do IL gerenciado. Todo o lado Android nativo (os wrappers Java gerados pelo tooling do MAUI, bibliotecas Kotlin de androidx.*, drawables) é responsabilidade do R8, o code shrinker do Android Gradle Plugin. Habilitar R8 no MAUI é uma linha (<AndroidLinkTool>r8</AndroidLinkTool>), mas configurar as regras de manutenção exige atenção.
Crie Platforms/Android/proguard.cfg e adicione ao csproj:
Um proguard.cfg mínimo para MAUI que integra com Firebase e OkHttp:
# MAUI/Xamarin renderers e handlers
-keep class mono.MonoRuntimeProvider { *; }
-keep class mono.android.** { *; }
# Firebase Messaging service
-keep class com.google.firebase.messaging.** { *; }
-keep class com.mycompany.myapp.services.MyFirebaseMessagingService { *; }
# OkHttp / Retrofit (comum em backends)
-dontwarn okhttp3.**
-dontwarn okio.**
-keepattributes Signature
-keepattributes *Annotation*
Para o resource shrinker, AndroidLinkResources=true ativa a passagem que remove drawables, layouts e strings não referenciadas. Se você tem PNGs legados do Xamarin.Forms nunca usados no MAUI (bem comum durante migrações; veja meu guia de migração do Xamarin.Forms para .NET MAUI), o shrinker corta sem dó. Em um dos apps que otimizei, esse único passo removeu 4,2 MB de imagens antigas que ninguém tinha coragem de deletar manualmente.
Divisão por ABI e remoção de arquiteturas desnecessárias
Cada ABI presente no APK duplica todas as libs nativas: libmonosgen, libSkiaSharp, libSystem.Native, mais qualquer .so de bibliotecas nativas de terceiros. Para dispositivos Android modernos, as ABIs relevantes são exatamente duas:
arm64-v8a: obrigatória. Todos os dispositivos publicados na Play Store desde agosto de 2019 são 64-bit.
armeabi-v7a: opcional. Cobre uma cauda longa de dispositivos antigos em mercados emergentes. Se seu público não inclui esse segmento, remova.
As ABIs x86 e x86_64 só fazem sentido para emuladores. Não distribua por Play Store. Configure explicitamente no csproj:
No AAB isso é irrelevante para o usuário final (a Play Store gera splits automaticamente), mas ainda importa por dois motivos: reduz o tamanho do .aab que sobe (upload mais rápido no CI), e reduz o APK universal que você distribui em canais alternativos como Amazon Appstore ou Huawei AppGallery. Em um dos meus apps, cortar x86_64 economizou 14,6 MB no APK universal.
Como analisar o conteúdo de um APK MAUI
Otimização às cegas é chute. Antes e depois de cada mudança, meço com duas ferramentas:
APK Analyzer (Android Studio)
Abre o APK como uma árvore navegável, mostrando tamanho por diretório e por arquivo. Útil para identificar assemblies grandes suspeitos (um Newtonsoft.Json.dll aparecendo com 700 KB depois do trimming é sinal de que a preservação está exagerada). Acesse via Build > Analyze APK....
bundletool para APKs a partir de um AAB
Se você publicou como AAB, a métrica que importa é o tamanho de download por dispositivo, não o tamanho do bundle. Gere os splits localmente:
# Instalar bundletool
brew install bundletool
# Gerar todos os APKs que a Play Store geraria
bundletool build-apks \
--bundle=./bin/Release/net9.0-android/publish/com.mycompany.myapp.aab \
--output=./output.apks \
--mode=default \
--ks=./keystore.jks --ks-key-alias=upload
# Extrair o splits.json com tamanhos
bundletool get-size total --apks=./output.apks --dimensions=SDK,ABI,LANGUAGE,SCREEN_DENSITY
O resultado é uma tabela que mostra o download real que um Pixel 8 vai fazer versus um Galaxy S10 versus um Redmi 9A. É essa a métrica que você reporta em standup, não o tamanho do .aab. A documentação oficial em bundletool no site do Android tem a lista completa de comandos.
Erros comuns após habilitar trimming
Nas primeiras vezes que ativei TrimMode=full em produção, quebrei três coisas em três releases seguidos. Aqui vão os padrões que sempre reviso:
Deserialização silenciosa que retorna null.System.Text.Json sem source generator perde metadados de propriedades. Solução: use JsonSerializerContext ou preserve o assembly de DTOs.
NotSupportedException: Reflection with dynamically accessed members. Alguma dependência chama Activator.CreateInstance em tipo dinâmico. Localize pelo stack trace e adicione [DynamicDependency] ou preserve o tipo.
DI containers. Se usa Microsoft.Extensions.DependencyInjection com registrar por convenção (reflection sobre assembly), migre para registro explícito ou preserve o assembly.
Bindings XAML quebrados. Raro em MAUI (compile-time bindings ajudam), mas veja tags Binding que referenciam propriedades de modelos preservados. Use x:DataType em toda página para compile-time binding. É uma recomendação que também aparece no meu artigo sobre otimização de performance no .NET MAUI.
Culturas de globalização. Por padrão o trimming remove ICU e mantém só invariant culture. Se você formata datas/moedas por locale, adicione <InvariantGlobalization>false</InvariantGlobalization>.
Estudo de caso: de 84 MB para 31 MB
Este é o caso que apresentei no NDC Oslo 2025. O app fintech em que trabalho tinha um APK universal de 84 MB depois de dois anos migrando do Xamarin.Forms, e muito desse peso vinha de bibliotecas legadas e resources não usados que sobreviveram à migração (sério, tinha PNG de 2018 lá dentro). Meta interna: chegar abaixo de 35 MB sem cortar recursos.
Ordem em que apliquei as mudanças, com o impacto medido em cada passo:
Baseline (Xamarin.Forms era): 84,2 MB
Migração para MAUI .NET 9 (sem otimizações): 71,8 MB (-12,4 MB)
Migração para JsonSerializerContext (removeu Newtonsoft.Json): 31,4 MB (-1,9 MB)
Total: 52,8 MB cortados, 62,7% de redução. O maior ganho isolado foi o trimming; o mais fácil (uma linha no csproj) foi remover x86_64. Publicando como AAB, o download que o Pixel 6 do meu colega mostra na Play Store hoje é 24,1 MB.
Nada disso é ilusão. Um erro comum (eu já cometi, aliás) é reportar o .aab como se fosse tamanho de instalação. O AAB do meu app tem 42 MB, o que soa terrível. O que importa mesmo é o get-size total do bundletool, ou o número que a Play Store mostra ao usuário. Meça essa métrica desde o começo. Se seu pipeline ainda não gera esse relatório automaticamente, adicione um step. O meu guia de CI/CD para .NET MAUI com GitHub Actions tem o exemplo de workflow que uso para publicar o size report como PR comment.
Perguntas Frequentes
Qual o tamanho mínimo realista de um app .NET MAUI para Android?
Um app MAUI "Hello World" com todas as otimizações ligadas (trimming full, R8, AAB, arm64-only) baixa hoje na Play Store em torno de 12–15 MB. Um app real de produção com Firebase, um HTTP client, SQLite e algumas telas fica na faixa de 18–25 MB. Abaixo disso é conto de fadas.
O que é trimming no .NET?
Trimming é o processo pelo qual o ILLink analisa o grafo de dependências do seu app a partir do ponto de entrada e remove IL não alcançável estaticamente. No modo full, tudo que só é acessado via reflection é considerado morto. Reduz o tamanho dos assemblies em 30–60% mas exige atenção a serialização e DI.
Preciso habilitar AOT para reduzir o tamanho?
Não. AOT (Ahead-of-Time compilation) faz o oposto: aumenta o tamanho do binário porque converte IL em código nativo. Você habilita AOT para reduzir tempo de startup, não tamanho. Combinado com AndroidStripILAfterAOT=true o custo é atenuado, mas nunca vira ganho líquido.
Como reduzir o tamanho de um app MAUI para iOS?
Muitas técnicas se aplicam (trimming, remoção de assets não usados, MtouchLink=SdkOnly ou Full), mas o formato final é IPA e a distribuição via App Store já cuida de thinning (App Slicing) automaticamente. O ganho de otimização manual em iOS costuma ser menor que em Android: 20–30% em vez de 60%.
O ProGuard ainda é usado ou foi substituído pelo R8?
R8 substituiu ProGuard como shrinker padrão do Android Gradle Plugin desde 2019 e é o que o .NET MAUI usa quando você define AndroidLinkTool=r8. A sintaxe do arquivo de configuração (proguard.cfg) é a mesma por compatibilidade, então documentação antiga de ProGuard continua válida.
Sofia is a mobile platform engineer with eleven years across native iOS, Xamarin, and now .NET MAUI. She spent three years at Spotify in Stockholm working on internal tooling for the mobile build infrastructure, then joined a fintech in Milan where she leads the mobile foundations team responsible for a MAUI app that handles around 2 million monthly active users across iOS and Android.
Most of what she writes about lives in the build and release layer: deterministic builds, fastlane integration for MAUI, code signing on macOS runners, MAUI .NET 9 upgrade postmortems, and benchmarking startup time on cheap Android hardware. She co-organizes the Milano .NET meetup and gave a talk at NDC Oslo 2025 on shrinking a MAUI Android APK from 84 MB to 31 MB without losing features.
Guia prático de otimização de performance no .NET MAUI 10: NativeAOT, CollectionView virtualizado, redução de cold start, correção de memory leaks e diminuição do tamanho de APK/IPA com código real.