Reducing Flutter App Size: Measure First, Then Trim
How I approach reducing Flutter app size — measuring with --analyze-size, app bundles, split debug info, asset and font trimming, and auditing heavy packages.

Someone opens your app’s store page on a phone that’s nearly full, on mobile data, and sees the download size. That number decides whether they tap “Install” now, later, or never.
Flutter apps have a reputation for being big. Some of that is fair — every app ships the Flutter engine — but most of the bloat I find when I take over an existing Flutter app has nothing to do with Flutter. It’s oversized images, fonts nobody uses, debug symbols shipped by accident, and packages pulled in for one function.
Here’s how I find and fix it, in the order I’d actually do it.
Step 1: Measure the right thing
Before changing anything, get a real number and a breakdown. The size of the debug APK on your machine is meaningless — debug builds include the JIT, assertions and extra tooling, and are many times larger than release builds.
Flutter can produce a size analysis for a release build:
flutter build appbundle --analyze-size --target-platform android-arm64
flutter build ios --analyze-size
This prints a summary to the terminal and writes a JSON file. Open DevTools (dart devtools), go to the App Size tool, and load that file. You get a treemap of the Dart code by package and library, plus the native and asset parts of the bundle. You can also load two analysis files and diff them, which is how I check that a change actually helped.
The first time you do this on a real project, the treemap usually tells you where to look within a minute.
Step 2: Understand which size users see
There are several “sizes”, and people mix them up constantly:
- Download size is what goes over the network. It’s compressed.
- Install size is what ends up on the device after unpacking. It’s larger.
- Your local build artifact (an
.aabor.ipa) is neither. An App Bundle contains code for every CPU architecture and screen density; no user ever downloads all of it.
The numbers that matter are the ones the stores report:
- Google Play Console has an app size report showing download and install sizes for common device configurations.
- App Store Connect shows sizes per device after thinning. Locally, exporting an archive from Xcode with app thinning enabled produces an App Thinning Size Report with the same breakdown.
One iOS-specific surprise: App Store binaries are encrypted, and encrypted data compresses poorly. So iOS download sizes often look larger relative to the build than Android’s do. That’s normal, not a sign you did something wrong.
Step 3: Ship App Bundles, not fat APKs
If you’re still uploading a universal APK to Google Play, stop. Upload an App Bundle (flutter build appbundle). Play generates optimized APKs per device, so a user on a 64-bit ARM phone downloads only the ARM64 native code and only the resources for their screen density.
If you distribute APKs outside Play — to testers, or through another store — split them per ABI instead of shipping one that contains everything:
flutter build apk --split-per-abi
This produces separate APKs for armeabi-v7a, arm64-v8a and x86_64. Each one is noticeably smaller than the universal build.
Step 4: Move debug info out of the binary
Release builds can still carry symbol information that’s only useful for reading stack traces. Split it out:
flutter build appbundle \
--obfuscate \
--split-debug-info=build/symbols
--split-debug-info writes symbol files to the given directory instead of bundling them, which trims the compiled Dart code. --obfuscate renames symbols, which also makes reverse engineering a little harder.
The catch: you must keep those symbol files for every release, or obfuscated crash reports become unreadable. Archive them in CI alongside the build and upload them to your crash reporting service. I cover that setup in crash reporting and monitoring in Flutter, and the CI side in Flutter CI/CD with Azure DevOps.
Step 5: Fix images and assets
In most of the apps I audit, assets are the single biggest category. Common culprits:
- PNG photos. Photos should be JPEG or WebP. Flutter decodes WebP on both platforms, and converting a folder of photos often shrinks it substantially without visible loss.
- Images far larger than displayed. A 4000-pixel-wide hero image displayed at 400 logical pixels wastes space on disk and memory at runtime. Export at the size you need.
- Resolution-aware assets done halfway. Flutter supports variants like
images/logo.png,images/2.0x/logo.png,images/3.0x/logo.pngand picks the right one per device. That’s great — but with plain Flutter asset bundling, all variants ship to every device. Don’t add a 4.0x variant “just in case”. - Simple graphics as bitmaps. Icons and illustrations are often smaller as vectors. The flutter_svg package handles SVGs.
- Dead assets. Files that were used two redesigns ago are still declared in
pubspec.yaml. Declaring a whole directory makes this easy to miss, because everything in it gets bundled.
Large, optional content — video tutorials, big image packs, offline map data — usually doesn’t belong in the app at all. Download it on demand and cache it.
Step 6: Trim fonts
Fonts add up fast, especially when a project bundles every weight of a family and uses two of them. Check which weights and styles your theme actually references and remove the rest.
For fonts with large character sets — CJK fonts are the extreme case — consider subsetting to the characters you need, with a tool like pyftsubset from fonttools. Be careful with user-generated content, though: subsetting a font that renders names or messages will break characters you didn’t anticipate. If you support many languages, test every locale after subsetting.
Icon fonts are already handled for you: release builds tree-shake icon fonts by default, keeping only the Icons and CupertinoIcons glyphs you reference. If your build logs say icon tree-shaking was skipped, look for an IconData built from a non-constant value — that prevents Flutter from knowing which glyphs are used.
Step 7: Audit heavy packages
Back in the DevTools treemap, look at Dart code by package, and check native dependencies too. The usual offenders:
- An SDK that bundles large native libraries (some ML, video and ad SDKs are much bigger than they look on pub.dev).
- Two packages that do the same thing because two developers added them at different times.
- A whole utility library added for a single function you could write in ten lines.
For each heavy dependency, ask whether it’s earning its place. Sometimes the answer is yes — a payments SDK is non-negotiable. But it’s worth knowing what each one costs.
Step 8: Deferred loading, where it applies
Dart supports deferred imports (import 'feature.dart' deferred as feature;), which load code only when you call feature.loadLibrary().
- On the web, this splits your JavaScript or WebAssembly output into separate chunks, so the initial load is smaller. It’s genuinely useful for large web apps.
- On Android, Flutter’s deferred components build on this with Play Feature Delivery, letting you download parts of the app (Dart code and assets) after install. It requires extra setup in
pubspec.yamland the Android project, and only works for apps distributed through Google Play. - On iOS, deferred components aren’t supported; deferred imports simply load from the main bundle.
Deferred components add real complexity. I’d only reach for them for large, rarely used features — and only after the simpler steps above are done.
Takeaways
- Measure release builds with
--analyze-sizeand the DevTools App Size tool; never judge by debug builds. - Track the download and install sizes the stores report, not your local artifact.
- Upload App Bundles to Play, and use
--split-per-abifor APKs you distribute yourself. - Use
--split-debug-infoand--obfuscate, and archive the symbol files for every release. - Assets and fonts are usually the biggest easy wins; heavy packages are next.
- Treat deferred components as a last resort, not a first step.
If your Flutter app has grown bigger than it should be and you’d like a second pair of eyes on where the weight is coming from, get in touch.
Comments
Questions, corrections or your own experience — leave a comment below (GitHub sign-in).