All Articles
CategoryFlutter
Reading Time
14 min read
Published
2024-11-12
Word Count
3,104words

Grab a coffee — this one is a deep dive!

Flutter App Size Reduction: The APK and IPA Guide

Summary

A practical, documentation-based guide to Flutter app size reduction, covering --analyze-size, --split-debug-info, --obfuscate, split APK, and app bundle strategies step by step.

  • Take a baseline first with flutter build apk --analyze-size; never trust an optimization you haven't measured.
  • --split-debug-info can dramatically shrink code size; it can be combined with --obfuscate.
  • App bundle for Play Store distribution, --split-per-abi outside it — both beat a fat APK.
  • iOS App Thinning Size Report gives a projected size; only a release IPA uploaded to App Store Connect gives the exact figure. IPAs are usually bigger than APKs due to engine packaging differences.
Flutter App Size Reduction: The APK and IPA Guide

Before getting into Flutter app size reduction, one thing needs saying plainly: "why is my APK three times the size of the team next door's" rarely has a single answer — it's usually hidden across three cumulative sources: the Flutter engine, the code Dart AOT compilation includes, and the assets added to the project. This guide covers --analyze-size, --split-debug-info, --obfuscate, --split-per-abi, and app bundle strategies step by step, based on the official Flutter documentation.

💡 Pro Tip: Before you start shrinking size, always run flutter build apk --analyze-size to get a baseline — that way you'll see with numbers, not speculation, which change actually works.

Table of Contents

Where Does App Size Come From?

A Flutter release build's size comes from three main components: the Flutter engine itself (written in C++, includes the Skia/Impeller render layer), the machine code produced by the Dart Ahead-of-Time (AOT) compiler, and the images/fonts/JSON-like assets you add. The first two are essentially fixed regardless of platform and architecture; what you can actually control is your own code and your assets.

Debug builds are a misleading reference point. Flutter's official docs state this explicitly: debug build size is large because of the overhead needed for hot reload and source-level debugging, and it doesn't represent production size. So before panicking that "my debug APK is 80 MB," always check a file produced with flutter build apk --release.

How Do Dart AOT and Tree-Shaking Work?

During a release build, the Dart compiler applies aggressive tree-shaking: if a code path can be proven to never run at compile time, it's excluded from the final binary. The classic official example is platform checks written with the Platform class in dart:io: wrap a Windows-only block in Platform.isWindows and build for Android, and the compiler sees that condition is always false, stripping the Windows code entirely. So "every line you write adds to the size" isn't always true — what matters is whether that code can be eliminated at compile time.

  • Engine share: the platform runtime + render layer, largely fixed from project to project
  • Dart AOT share: the compiled form of your own code + dependencies, open to tree-shaking
  • Asset share: images, fonts, JSON/lottie files — usually the layer that's easiest to shrink

Measuring With the DevTools App Size Tool

Instead of guessing, measure — using Flutter's own built-in tool. Available since Flutter 1.22 and DevTools 0.9.1, the size analysis tool produces a complete breakdown of the release build and visualizes it in DevTools.

The command supports multiple platform targets:

bash
1# Size analysis for Android APK
2flutter build apk --analyze-size
3 
4# Size analysis for Android App Bundle
5flutter build appbundle --analyze-size
6 
7# Size analysis for iOS
8flutter build ios --analyze-size
9 
10# Desktop targets
11flutter build linux --analyze-size
12flutter build macos --analyze-size
13flutter build windows --analyze-size

This prints a size summary to the terminal and produces a *-code-size-analysis_*.json file (e.g. ~/.flutter-devtools/apk-code-size-analysis_01.json). Load this JSON into DevTools' "App Size" tab for a detailed treemap and table inspection.

The App Size tool has two tabs: Analysis, where you inspect a single snapshot, and Diff, where you compare two snapshots. The code-attribution views in the Analysis tab:

  • Dominator tree: used to understand the root cause of _why_ a piece of code is present in the binary
  • Call graph: used to see all the call paths going into/out of a piece of code

The Diff tab is used to compare two different --analyze-size outputs (for example, before and after an optimization) and visualize the difference in a treemap and table.

The practical workflow: take a JSON before optimizing, make the change, run --analyze-size again, compare the two files in Diff. This answers "did this change actually save 2 MB, or does it just feel that way" with a number.

Shrinking Code Size With --split-debug-info and --obfuscate

The official documentation directly recommends --split-debug-info for shrinking code size, stating it can reduce it _dramatically_. This flag moves debug symbol information out of the binary into a separate directory; the app itself no longer carries these symbols, but you can still symbolicate crash reports.

bash
1# Move debug info to a separate directory (size reduction)
2flutter build apk --release \
3 --split-debug-info=build/debug-info
4 
5# Do the same together with obfuscation
6flutter build apk --release \
7 --obfuscate \
8 --split-debug-info=build/debug-info

Obfuscation (--obfuscate) only works in release builds and, as the docs explicitly warn, doesn't encrypt your source or fully prevent reverse engineering — it only makes symbol names harder to read. It isn't designed to shrink size directly, but combined with --split-debug-info, both become standard parts of the release pipeline.

Obfuscation-supported build targets include apk, appbundle, ios, ios-framework, ipa, linux, macos, macos-framework, windows, and aar — spanning mobile, desktop, and framework/AAR packaging. Web apps don't support obfuscation; minification is applied instead, giving a similar "reduced readability" effect.

⚠️ Warning: to resolve the stack trace of a crash while obfuscation is active, keep the symbol files produced by --split-debug-info somewhere safe (e.g. a CI artifact store); otherwise crash reports arrive full of meaningless names.

Split APK by ABI, or App Bundle?

On Android, the single biggest decision affecting size is which architectures (ABIs) you build for. By default, an app bundle carries your Dart code and the Flutter runtime compiled for _all_ of armeabi-v7a (ARM 32-bit), arm64-v8a (ARM 64-bit), and x86-64.

An unflagged (default) flutter build apk produces a "fat APK" merging all these architectures into one file. It offers broad device compatibility, but its size is much larger than a single-architecture APK. The docs give a clear recommendation: even distributing outside the Play Store, producing a split APK with --split-per-abi is strongly recommended.

bash
1# Fat APK (all ABIs in a single file, largest size)
2flutter build apk --release
3 
4# Split APK (a separate, smaller file per ABI)
5flutter build apk --release --split-per-abi
6 
7# App Bundle (the format the Play Store recommends, most efficient delivery)
8flutter build appbundle --release

With --split-per-abi, three separate APK files are produced: one each for armeabi-v7a, arm64-v8a, and x86_64. Worth noting: with split APKs, Flutter automatically adds ABI_VERSION * 1000 to the version code, preventing the Play Store from rejecting multiple APKs with the same version code.

Another Android-side reduction layer is the code shrinker. Google's R8 is enabled by default in release APK/AAB builds; --[no-]shrink has no effect, since code shrinking can't be disabled in release (the docs' own note). R8 also indirectly helps with the dex method limit: projects targeting minSdk 20 and below, with many packages or a large codebase, risk hitting Android's 64k method dex limit. Flutter tooling detects this automatically, catches the multidex build error, and asks your approval before changing your Android project — so manual configuration is rarely needed.

The Google Play Store has required new apps to publish as App Bundle (AAB) since August 2021, and the docs recommend app bundle over APK: Play dynamically generates a device-specific APK server-side and delivers only what that device needs — automating what you'd otherwise do manually with a split APK.

Format
Who produces it
File that lands on the user's device
When to prefer it
Fat APK
flutter build apk
A single file containing all ABIs
Testing/distribution outside the Play Store, quick prototyping
Split APK
flutter build apk --split-per-abi
A single file matching the device's ABI
Distribution channels outside the Play Store (sideload, enterprise)
App Bundle
flutter build appbundle
The optimized package the Play Store dynamically produces
Official distribution through the Play Store (default recommendation)

Image Asset Strategy: Resolution Variants

In most projects, assets take up as much size as code — sometimes more. Specifying a directory (directory/) under assets: in pubspec.yaml bulk-includes every file in that folder at once; this is less error-prone than adding files one by one, but remember that everything in that directory ends up in the build.

Flutter's resolution-aware asset mechanism is one of its most established features, automatically choosing the right variant based on the device's pixel density:

  • my_icon.png: the base (mdpi) resolution
  • 1.5x/my_icon.png: for hdpi devices
  • 2.0x/my_icon.png: for xhdpi devices
  • 3.0x/my_icon.png: for xxhdpi devices
  • 4.0x/my_icon.png: for xxxhdpi devices

This is far more efficient than "send one giant image to every device and let Flutter scale it down"; instead of runtime scaling, the correctly sized file is bundled directly. I generally start from the highest resolution (3.0x or 4.0x) and downscale when needed, rather than producing lower-density variants by hand — this keeps quality loss minimal.

Measuring IPA Size on iOS

Measuring size on iOS follows a different flow than Android. Running Xcode's "Distribute App" flow (App Thinning) produces a file named App Thinning Size Report.txt, listing the app's _projected_ size across devices and iOS versions in detail. The only reliable way to measure an iOS app's exact size is uploading a release IPA to App Store Connect and reading the size report from there.

bash
1# Produce a release IPA for iOS, with obfuscation and debug-info split
2flutter build ipa \
3 --release \
4 --obfuscate \
5 --split-debug-info=build/ios-debug-info

The docs recommend adding --obfuscate and --split-debug-info on iOS release builds too, for the same reverse-engineering-resistance logic as Android.

A frequent question: why is my IPA bigger than my APK? Flutter's docs confirm this is a general pattern: IPAs are commonly larger than APKs due to a cross-platform size difference in the engine. It's not your code's fault — it's a structural difference in how the two platforms package the engine — so seeing "12 MB on Android, 22 MB on iOS" isn't cause for panic; check the actual breakdown in the App Size report.

Auditing Package Dependencies by Size

Every package you add to pubspec.yaml potentially adds code and assets to your size — but measuring beats guessing. This is where the dominator tree view comes in: it shows the root cause of why a package (or a specific class within it) is present in the binary.

The practical audit workflow goes like this:

  1. Produce a JSON of the current state with flutter build apk --analyze-size
  2. Load the JSON into the DevTools App Size tool and identify the packages taking up the largest share in the treemap
  3. Select a suspicious package in the dominator tree and examine which import is including it in the binary
  4. Remove/replace the unused package or one that has a much lighter alternative, then run --analyze-size again
  5. Compare the two JSONs in the Diff tab to verify the actual gain

This loop ties every change to measurable evidence rather than intuition like "this package looks big, let's remove it" — on larger teams, it's the only reliable way to settle "who bloated the size" arguments with a number.

Step-by-Step Size Reduction Checklist

Since every app's asset load, dependency graph, and target ABI set differ, a single generic before/after table would mislead; produce your own by following the steps below with the DevTools flow described above.

Step
Optimization applied
Expected effect
1
Measure a baseline with --analyze-size
No measurement = no optimization
2
Add --split-debug-info
Dramatic reduction in code size (the documentation's own wording)
3
flutter build appbundle (instead of a fat APK)
Play Store's device-specific delivery, smaller download for the user
4
--split-per-abi (for distribution outside Play Store)
Significantly smaller single-ABI files compared to a fat APK
5
Organize assets with resolution variants
Prevents unnecessarily large images from being carried around at runtime
6
Audit dependencies with the dominator tree
Detection and removal of unused/heavy packages

GOLDEN TIP

The most valuable insight in this article

This tip holds the article's most important takeaway.

Easter Egg

You found a hidden gem!

There's a hidden detail in this section. Want to uncover it?

Reader Reward

You can use the list below as a checklist before your next release; each item corresponds to a documentation-verified step described in this article.

FAQ

Why is my Flutter app so large?

Most of the size comes from three sources: the Flutter engine, the code included by Dart AOT compilation, and the project's assets. The engine's share is largely fixed per platform; you control your own code (open to tree-shaking) and your assets most. Judging size from the debug build is misleading, since it carries overhead for hot reload support.

How do I reduce Flutter APK size?

The three most effective steps, in order: move debug symbols out of the binary with --split-debug-info (the docs' own "dramatic reduction" step), produce an ABI-specific split APK with --split-per-abi instead of a fat APK outside the Play Store, and use an app bundle directly for Play Store distribution. Don't trust any change without measuring it with --analyze-size.

What's the difference between App Bundle and split APK?

A split APK consists of separate files, each for a single ABI, that you manually produce with --split-per-abi — suitable for channels outside the Play Store. An App Bundle is the optimized package format the Play Store itself dynamically produces per device; Google Play officially recommends app bundle over APK for a more efficient delivery to users.

Which assets bloat size in Flutter?

The most common culprits are images added as a single large file without resolution variants. Adding a whole directory under assets: in pubspec.yaml is practical, but remember every file in it gets included in the build; an uncontrolled, growing asset folder can overshadow every code-side optimization. The treemap view in DevTools App Size directly shows which asset takes the largest share.

Update (September 2026)

This article was written based on the Flutter version as of 2024-11-12. Since then, two verified changes hit the render and design layer: with Flutter 3.47, Impeller became the default render engine on desktop (macOS, Linux, Windows) — you can revert to the old behavior in debug with --no-enable-impeller, and in distribution with a platform-specific setting (macOS: FLTEnableImpeller in Info.plist, Linux: fl_dart_project_set_enable_impeller, Windows: set_impeller_switch), though this opt-out is planned for removal later (source: docs.flutter.dev/perf/impeller). Same release, the Material and Cupertino design libraries split out of the core SDK into independent pub.dev packages, package:material_ui and package:cupertino_ui; migrating is optional and can be automated with dart fix --apply --code=migrate_design_widgets (source: docs.flutter.dev/release/breaking-changes/material-ui-and-cupertino-ui). Neither change is claimed to directly affect APK/IPA size figures, but projects migrating their render engine or design package should re-run this article's --analyze-size flow afterward for a new baseline.

Conclusion

Shrinking Flutter app size isn't about a single magic flag — it's a measurement habit: take a baseline with --analyze-size, tighten the code side with --split-debug-info and, if needed, --obfuscate, prefer app bundle for Play Store distribution, use --split-per-abi outside the Play Store, and organize assets with resolution variants. For similar measurement discipline on runtime performance, see our Flutter performance optimization guide; for an equivalent iOS flow, our iOS app size optimization article complements this one directly. Setting up the architecture correctly from the start also makes size discipline easier — our Flutter clean architecture guide and Flutter testing strategy guide help here. To assess size impact before adding a heavy SDK like Firebase, see our Flutter Firebase integration guide. For launch speed alongside size, our iOS app launch optimization article shares this same measurement philosophy.

Sources

Tags

#Flutter#APK#IPA#app bundle#DevTools#obfuscation#performance#Android
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

Lead Mobile Engineer with 12+ years of experience. Expert in iOS, Android and cross-platform architectures with Swift, SwiftUI, Kotlin and Flutter. I build performant, user-friendly mobile apps.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

We respect your privacy. You can unsubscribe at any time.

Share