To reduce .NET MAUI app size in 2026, enable full IL trimming with <TrimMode>full</TrimMode>, turn on R8 with resource shrinking for Android, switch to Android App Bundles (.aab) for Play Store distribution, and rely on iOS App Thinning by shipping vector or SVG assets instead of multi-resolution PNGs. A default .NET MAUI 10 Release build with these four levers turned on typically drops from ~85 MB to ~22 MB on Android and from ~75 MB to ~30 MB on iOS. I've shipped this exact playbook on three production apps and the gains were consistent. The catch? Trimming is also where most teams break their app, so the order matters.
A stock .NET MAUI 10 Release APK is ~80–90 MB before optimization. With full trimming, R8, and AAB it lands around 18–25 MB user-installed.
Set <PublishTrimmed>true</PublishTrimmed> and <TrimMode>full</TrimMode> for Release builds, but expect reflection-heavy libraries (Newtonsoft.Json, AutoMapper, some DI containers) to throw at runtime unless you add TrimmerRootAssembly entries.
Android Native AOT is not supported in .NET 10 for MAUI; use Mono AOT with RunAOTCompilation=true and AndroidEnableProfiledAot=true for ~30% smaller startup-critical code.
On iOS, ship a single .car asset catalog and let App Thinning slice per-device variants. Never ship @1x/@2x/@3x PNGs manually in 2026.
Use dotnet publish -f net10.0-android -c Release with -p:AndroidPackageFormat=aab and inspect the result with bundletool before submitting.
Why .NET MAUI apps are so large by default
So, when you publish a stock .NET MAUI 10 Release build with no optimization flags, the Android APK lands around 80–90 MB and the iOS IPA around 70–80 MB. The reason isn't bloat in your code. It's the .NET runtime, the BCL (Base Class Library), the MAUI handlers for every platform-specific control, and a debug-friendly default trim mode that keeps almost everything. A "Hello World" MAUI app weighs more than most full Flutter or React Native apps because Mono and the BCL ship in full unless you tell them not to.
There's also a compatibility tax. MAUI multi-targets to four platforms (Android, iOS, MacCatalyst, Windows), and each TargetFramework brings its own AndroidX, Cocoa interop, and resource set. A library you reference once may pull in three platform-specific NuGet packages. Add in the fact that the default project template enables the Mono interpreter (for hot reload) and you have a baseline that is wildly larger than what your users actually need to run your app. The good news? Every gram of that fat is removable with the right combination of flags, and the tooling has improved meaningfully in .NET 10.
How to measure your .NET MAUI app size accurately
You cannot optimize what you cannot measure, and the wrong measurement leads to wrong conclusions. The size of the file your build produces is almost never the size your user downloads. For Android, the .aab on disk is larger than what Google Play delivers, because Play splits it per ABI, screen density, and language. For iOS, the .ipa from Xcode includes every architecture and every asset variant; the App Store's App Thinning picks one slice per device.
Use bundletool for Android to simulate what a real device downloads:
bundletool build-apks --bundle=app.aab --output=app.apks --mode=universal
bundletool get-size total --apks=app.apks --dimensions=ABI,SCREEN_DENSITY
For iOS, generate an App Thinning Size Report from Xcode Organizer, or use xcrun altool in CI. App Store Connect itself shows the install size on the per-build screen after upload, and that is the number to track. Honestly, I keep a running CSV per release with three columns: build size on disk, simulated install size, and store-reported install size. The third number is the only one that matters to users, but the first two move faster, so they're better for catching regressions early.
IL trimming and the TrimMode trade-off
IL trimming is the single biggest lever for .NET MAUI app size, and also the most dangerous. The trimmer statically analyzes your IL and removes unreachable code; on a MAUI app it can cut 40–60 MB. The cost is that reflection-based code paths are invisible to static analysis, so anything that uses Activator.CreateInstance, Type.GetType, or runtime serialization will throw System.MissingMethodException or silently fail.
The two TrimMode values you care about are full (aggressive, trim every assembly) and partial (conservative, only trim assemblies marked trim-safe). full saves roughly twice as much space, but you'll need to opt assemblies back in when something breaks. Do this with:
Set TrimmerSingleWarn=false so the build prints every IL2026/IL2104 warning instead of grouping them. Fix or suppress every warning before shipping. A single unhandled IL2026 against a library you actually use is a runtime crash waiting to happen. The official trim warning reference documents what each code means and how to silence it correctly. Treat warnings the way you'd treat compiler errors. There is no "we'll get to it later" with trimming.
AOT compilation: Mono AOT, ReadyToRun, and Native AOT in 2026
AOT (Ahead-Of-Time) compilation converts IL to native code at build time. There are three flavors that show up in .NET MAUI discussions, and they are not interchangeable. Native AOT (the new .NET 8+ model with no Mono runtime) is still not supported for MAUI on Android in .NET 10, because the MAUI handler infrastructure relies on Mono. iOS AOT is mandatory and always on (the App Store rejects JIT). Android uses Mono AOT, which is opt-in.
For Android Release, enable profiled AOT. It AOT-compiles only the methods your app actually executes at startup, hitting most of the perf benefit without the size penalty of full AOT:
<PropertyGroup Condition="'$(Configuration)' == 'Release' and $(TargetFramework.Contains('-android'))">
<RunAOTCompilation>true</RunAOTCompilation>
<AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
<AndroidStripILAfterAOT>true</AndroidStripILAfterAOT>
</PropertyGroup>
AndroidStripILAfterAOT is the one most teams miss. It removes the original IL after AOT compilation, since the AOT-compiled native code is already in the APK. Without this flag you ship both the IL and the native code, which is the worst of both worlds. With it, expect 6–10 MB shaved off and a ~25% faster cold start. If you need more startup speed than profiled AOT gives, you can use full AOT (AndroidEnableProfiledAot=false), but the APK grows 15–20 MB. For most apps, profiled AOT is the right answer; full AOT only makes sense when every millisecond of startup matters and you're willing to trade size for it. For more on the startup side of this trade-off, see my deep dive on .NET MAUI performance optimization.
Android: R8, resource shrinking, and App Bundles
R8 is the Java/Kotlin bytecode shrinker that Android Gradle uses, and yes, MAUI runs R8 over the AndroidX and Java interop code in your app. It's enabled by default in Release, but the resource shrinker is not, and that's where a lot of size hides.
AndroidLinkResources removes unused Android resources (drawables, layouts, strings), typically 3–8 MB of savings on an app that pulled in Material Design components. AndroidPackageFormat=aab produces an Android App Bundle instead of a universal APK, and the Play Store will then deliver per-device splits that are 30–50% smaller than the universal APK would be. There's no reason to ship APK to the Play Store in 2026 anyway; it's been required-AAB since August 2021 for new apps.
R8 ProGuard rules for MAUI
If you reference an Android library that uses reflection (most analytics SDKs do), you may need ProGuard rules to keep R8 from stripping it. Drop them in Platforms/Android/proguard.cfg and reference them in your csproj. Refer to Android's code shrinking guide for syntax; the keep rules you'd use in a native Android app work unchanged here. My CI/CD article also covers how to wire R8 mapping file upload into your pipeline so crash reports deobfuscate correctly.
iOS App Thinning, asset catalogs, and on-demand resources
iOS has solved a chunk of this problem for you, but only if you cooperate with it. App Thinning is the App Store's mechanism for delivering a per-device slice of your IPA. A user on iPhone 15 gets only the arm64 binary and @3x assets; an iPad mini user gets only what their device needs. The build size in App Store Connect (~75 MB) is misleading; the install size is typically 30–40 MB.
To make App Thinning work effectively:
Use Asset Catalogs. Put images in Resources/Images and let MAUI compile them into a .car. Asset catalogs are what App Thinning slices on; loose PNGs are not.
Ship SVG, not PNG, where possible. MAUI's MauiImage build action converts SVGs to platform-native formats (PDF for iOS, vector drawables for Android). One SVG replaces six PNGs.
Use MauiSplashScreen and MauiIcon. These build actions generate per-platform launch storyboards and icon sets automatically. Hand-managing a LaunchScreen.storyboard in 2026 is a code smell.
On-demand resources for large media. If you ship tutorial videos, language packs, or per-region content, mark them as BundleResource with on-demand tags. They download lazily after install.
You can also strip unused architectures for distribution builds. iOS apps only need arm64 for the App Store. Simulator builds need x86_64 and arm64; make sure your CI distinguishes them. The App Thinning developer guide from Apple is dated but still accurate on the core mechanics. If you're publishing for the first time, my walkthrough on publishing a .NET MAUI app to the stores covers the submission side end-to-end.
Asset and font optimization
Once code shrinking is dialed in, assets become the largest remaining chunk. Three quick wins, in order of impact:
1. Subset your custom fonts
A single TTF of MaterialIcons is ~350 KB. If you only use 40 icons, subset the font to those glyphs and you're down to ~12 KB. Tools like pyftsubset (from FontTools) or the online Font Squirrel Webfont Generator do this in seconds. Multiply by however many icon fonts you ship and you've found 1–2 MB.
2. Convert PNG to WebP on Android
WebP is 25–35% smaller than PNG with no quality loss. Android has supported lossless WebP since API 18, so unless you target ancient devices there's no compatibility excuse. Run cwebp -lossless over your Resources/Images folder before build. Make this part of your pre-commit hook so contributors can't slip a 600 KB PNG into the repo.
3. Audit the Lottie library
If you use Lottie animations in MAUI, the JSON files are tiny but the runtime libraries are not. SkiaSharp.Extended.UI.Maui with Lottie support adds ~4 MB. Use it if you ship at least 3–4 animations; for a single splash animation, hand-rolled SVG animation is smaller.
Auditing NuGet dependencies for bloat
I once shaved 18 MB off a production app by removing a single NuGet package: a CSV parser someone had added to read one config file at startup. Run dotnet list package --include-transitive regularly and ask of each package: "do we actually need this, and is there a smaller alternative?" A few patterns worth knowing:
Heavy package
Approx. size
Leaner alternative
Newtonsoft.Json
~2.5 MB
System.Text.Json with source generator
AutoMapper
~1.8 MB + trim risk
Hand-mapped DTOs or Mapperly (source-generated)
FluentValidation
~1.2 MB
DataAnnotations + custom validators
RestSharp
~1.5 MB
Refit (source-generated) or raw HttpClient
Polly v8 + Microsoft.Extensions.Http.Resilience
~3 MB
Polly v7 minimal or manual retry
None of these alternatives are objectively "better." Newtonsoft.Json is more flexible than System.Text.Json, and AutoMapper is more featured than hand-mapping. The point is that you have a trade-off to make consciously. On a free-tier mobile app where install size affects conversion, shaving 10 MB by swapping a few libraries is worth a day of refactoring.
Tracking app size in CI/CD
App size regressions are silent. Nothing breaks; the binary just creeps up 200 KB a sprint until you're 15 MB over where you started. The fix is to track it in CI like you'd track test coverage. Here's a minimal GitHub Actions step that fails the build if the AAB grows more than 5%:
Commit the baseline file and bump it deliberately when a feature genuinely justifies the growth. This is the single most impactful habit I've adopted across the three apps I mentioned at the top, because the conversation shifts from "we have to do a size cleanup sprint" to "this PR adds 300 KB; is that worth it?"
Common mistakes that re-inflate your binary
A few footguns I've stepped on personally (and watched teams step on):
Leaving the interpreter on.UseInterpreter=true is great for debug builds and hot reload. If it sneaks into Release, you ship the Mono interpreter (~3 MB) for nothing.
Including PDB files in Release. Set <DebugType>portable</DebugType> and <DebugSymbols>false</DebugSymbols> for Release. Upload PDBs to your crash service as a separate artifact instead.
Bundling fonts you don't use. Every EmbeddedFont entry ships. If you removed Tinker Bold from the design system, remove it from your resources too.
Forgetting to enable resource shrinking. R8 shrinks code by default in Release, but AndroidLinkResources is opt-in. Half the teams I've audited had this off.
Shipping multiple ABIs in a universal APK. If you must ship APK (sideloading, MDM), at least split per-ABI: AndroidSupportedAbis, RuntimeIdentifiers.
Frequently Asked Questions
Why is my .NET MAUI app so large compared to Flutter or React Native?
The .NET runtime, Mono interpreter, BCL, and per-platform MAUI handlers are bundled in by default. Without trimming and AOT they account for 40–60 MB on their own. Flutter ships its engine compiled to native code at much smaller size, and React Native leans on the device's JS engine. Once you enable full trimming, AOT, R8, and AAB, a MAUI app lands within 5–10 MB of a comparable Flutter app.
Does enabling trimming break .NET MAUI apps?
It can. Reflection-heavy libraries (older serializers, some DI containers, AutoMapper) lose their reachable types under TrimMode=full and throw at runtime. Fix it by adding TrimmerRootAssembly entries for those libraries, or by switching to source-generated alternatives (System.Text.Json source generator, Refit, Mapperly).
Should I use Native AOT for .NET MAUI in 2026?
No. Native AOT for MAUI on Android is not supported in .NET 10, and won't be until the MAUI handlers are rewritten without Mono dependencies. iOS already uses AOT under the hood (it has to). Stick with Mono AOT plus profiled AOT for Android; that's where the size and startup wins are realistic today.
How do I reduce iOS IPA size for the App Store?
Use asset catalogs (let App Thinning slice them), ship a single arm64 binary for the App Store, prefer SVG over multi-resolution PNG, and use on-demand resources for large optional media. The IPA you upload will be ~75 MB, but the install size that App Store Connect reports (and that users actually download) is typically 30–40 MB.
What is a reasonable target size for a .NET MAUI app?
For a content-driven app with reasonable third-party dependencies, target 20–30 MB user-installed on both platforms. Apps with offline maps, ML models, or bundled videos will be larger, and that's fine if it's actual feature payload. The red flag is when boilerplate (runtime, BCL, unused libraries) accounts for more than 50% of your install size; that's recoverable in a few days.
Learn how to detect, diagnose, and fix memory leaks in .NET MAUI apps. Covers finalizer logging, Visual Studio diagnostics, MemoryToolkit.Maui, event handler cleanup, and DisconnectHandler patterns with working code examples.
A practical guide to building accessible .NET MAUI apps — covering SemanticProperties, screen reader support for TalkBack, VoiceOver, and Narrator, WCAG compliance checklists, and working code examples.
A hands-on guide to implementing production-grade security in .NET MAUI apps — covering OAuth 2.0 with PKCE, MSAL.NET, biometric authentication, secure token storage, certificate pinning, and defense-in-depth patterns with working code.