AOT vs JIT Compilation
3 min read
Build & Compilation
| Flutter mode | Compilation | Hot reload | Asserts on | Use for |
|---|---|---|---|---|
Debug (flutter run) | JIT | ✅ | ✅ | Local iteration |
Profile (flutter run --profile) | AOT (mostly) + observatory | ❌ | ❌ | Performance measurement |
Release (flutter build … --release) | AOT | ❌ | ❌ | Ship to users |
| JIT (debug) | AOT (release) | |
|---|---|---|
| Compile time | Each function compiled on first call | All code compiled at build time |
| Startup | Slower (compile on demand) | Fast |
| Memory | Larger (compiler runtime present) | Smaller |
| Hot reload | ✅ | ❌ |
| Optimisations | Light (fast compile) | Heavy (inlining, tree shaking, devirtualisation) |
| Binary size | Larger | Smaller |
| Performance | ~10× slower in places | Production-grade |
What this means in practice
# Debug — fast iteration, useless for perf numbers
flutter run
# Profile — AOT-compiled, instrumented for DevTools
flutter run --profile
# Release — production binary, fully optimised
flutter build apk --release
flutter build ipa --release
Rule: Never benchmark in debug. Debug-mode timings include the JIT, extra assertions, type checks, layout verification — easily 10× the real cost. Always profile.
How AOT works at a glance
dart compile aot-snapshot main.dart(under the hood)- Produces a snapshot containing fully compiled native code, no Dart bytecode at runtime.
- Tree shaking strips unreachable code (see Q47).
- Constants are canonicalised and de-duped.
- The result is bundled into the app's native binary.
The flip side: no JIT means no dart:mirrors (already disabled), no hot reload, no eval — everything must be statically resolvable.
When each mode matters
| Question you're answering | Use mode |
|---|---|
| Is my UI wired up correctly? | Debug |
| Does my logic produce the right output? | Debug (with tests) |
| Why is this animation janky? | Profile |
| Why is startup slow on user devices? | Profile or release |
| What's the real APK/IPA size? | Release |
| Will tree shaking remove this code? | Release + --analyze-size |
Common mistakes to avoid
❌ Treating debug-mode frame times as real performance data
Debug adds layout assertions, type checks, raster overlays.
✅ Always profile.
❌ Forgetting hot reload doesn't work in profile/release
Quick UI iteration MUST be in debug; profile is for measurement.
❌ Shipping debug builds to users
Larger, slower, leaks debug info. Always build --release.
❌ Assuming AOT removes ALL runtime overhead
Bounds checks, null checks, allocations still happen.
Real perf wins still come from algorithmic fixes.
❌ Using `--enable-software-rendering` in profile
Disables GPU, skews everything. Only for debugging GPU issues.
Interview follow-ups
-
Why does hot reload only work in debug mode? Hot reload needs a JIT runtime — when you save changes, the framework recompiles only the changed classes and patches them into the running app. AOT-compiled native code can't be hot-patched, so release and profile builds don't support reload (or hot restart).
-
What's the difference between hot reload and hot restart? Hot reload swaps class definitions into the running VM — state is preserved,
initStatedoesn't re-run. Hot restart restarts the Dart VM frommain()— state is lost but you don't recompile native code. Both require JIT (debug mode). -
Why does profile mode exist if release exists? Profile is AOT-compiled (so timings are realistic) but keeps DevTools' Observatory connection alive, so you can attach the timeline and inspector. Release strips that out. Profile = "release perf with developer tools attached."
-
What's the difference between
dart compile exeand Flutter's AOT? Same compiler, different targets.dart compile exeproduces a standalone Dart binary for command-line tools. Flutter's AOT produces a snapshot bundled into the iOS/Android app and loaded by Flutter's engine. Same underlying technology.
How helpful was this content?
Please sign in to rate this article.