Skip to content

Build & Release

Flutter compiles the same code three different ways depending on what you need:

flowchart LR
  debug["debug
JIT · hot reload · asserts on
(slow, dev only)"]
  profile["profile
AOT · perf tools on
(measure real speed)"]
  release["release
AOT · asserts off · optimized
(ship this)"]
The three build modes and their compilation
  • Debug — JIT-compiled, hot reload, assertions on, service extensions available. Slow and large; for development only.
  • Profile — AOT-compiled like release, but with performance tooling still attached. This is where you measure real-world speed and hunt jank (never profile in debug — the numbers are meaningless).
  • Release — AOT-compiled, assertions stripped, debugging disabled, tree-shaken and optimized. This is what users get.

The debug/release split is really a compilation split:

  • JIT (Just-In-Time), used in debug, compiles Dart as it runs — which is what makes hot reload possible (it can swap in new code on the fly).
  • AOT (Ahead-Of-Time), used in profile and release, compiles Dart to native machine code before the app ships. The result is fast startup and predictable performance, at the cost of no hot reload.

Tree-shaking happens in AOT builds: unused code (and, notably, unreferenced icon glyphs) is stripped so the binary stays small.

Terminal window
flutter build apk --release # Android APK (direct install/testing)
flutter build appbundle --release # Android App Bundle (Play Store)
flutter build ipa --release # iOS archive (App Store)
flutter build web --release # web bundle

Each target produces a different artifact from the same Dart source — the framework abstracts the platform, the build step materializes it.

Flavors (Android) / schemes (iOS) let one codebase produce dev, staging, and production builds with different config — API endpoints, app IDs, icons — without branching your code.

Terminal window
flutter run --flavor dev --dart-define=API_URL=https://dev.api.example.com
flutter build apk --flavor prod --dart-define=API_URL=https://api.example.com

--dart-define passes compile-time constants your code reads via String.fromEnvironment — the clean way to inject per-environment values without committing secrets into source.

Which build mode should you use to measure real-world performance?
Why is hot reload possible in debug but not release?
What does tree-shaking do in a release build?
What are flavors used for?