---
title: "How Flutter Builds Your App"
description: "Debug, profile and release builds, JIT and AOT compilation, what's inside an Android App Bundle or iOS app (libapp.so, App.framework, the engine, assets), and which parts Code Push can update"
---

Flutter compiles the same Dart code in two very different ways: one for fast iteration while you develop, and one for speed and size when you ship. Knowing which one you’re running, and what ends up in the file you upload to a store, explains a lot: why hot reload doesn’t work in a release build, why a release build won’t run on an iOS simulator, and why Shorebird can update your Dart code but not your Kotlin or Swift.

## Debug, profile and release builds

Flutter has three [build modes](https://docs.flutter.dev/testing/build-modes):

| Mode        | How Dart runs                    | Use it for                                    | Built by                                          |
| ----------- | -------------------------------- | --------------------------------------------- | ------------------------------------------------- |
| **Debug**   | JIT-compiled in the Dart VM      | Development: hot reload, assertions, DevTools | `flutter run`                                     |
| **Profile** | AOT-compiled, tracing enabled    | Measuring performance on a real device        | `flutter run --profile`                           |
| **Release** | AOT-compiled, debugging stripped | What you ship to users                        | `flutter build <target>`, `flutter run --release` |

In **debug** mode, the Dart VM compiles your code just in time (JIT) as it runs. That is what makes hot reload possible: the tool sends changed code to the running VM, which swaps it in while keeping your app’s state. It is also why debug builds are large and can feel janky. Don’t judge performance or app size from a debug build.

In **profile** and **release** mode, Flutter compiles your Dart code ahead of time (AOT) into machine code for the device’s CPU (ARM or x64). The compiler starts from `main()`, removes code nothing can reach (tree shaking), and produces a native binary. The app starts fast and runs fast, and there is no compiler on the device to load new code into. Hot reload only works in debug mode, and profile and release builds don’t run on iOS simulators.

Shorebird and build modes

Shorebird only updates release builds. `shorebird release` builds in release mode, and a debug or profile build never checks for patches. See [Does Shorebird work in debug or profile mode?](/code-push/faq/#does-shorebird-work-in-debug-or-profile-mode)

## What’s inside a release build

A release build packages three kinds of content: your compiled Dart code, the Flutter engine with the native host app around it, and assets. Where each one lives depends on the platform.

### Android

`flutter build appbundle` produces an Android App Bundle (`.aab`). Inside it:

| Path in the bundle                    | What it is                                                       |
| ------------------------------------- | ---------------------------------------------------------------- |
| `base/lib/<abi>/libapp.so`            | Your Dart code, framework code and Dart packages, AOT-compiled   |
| `base/lib/<abi>/libflutter.so`        | The Flutter engine (C++), including the Dart runtime             |
| `base/dex/classes*.dex`               | Java and Kotlin code: your `MainActivity`, plugins’ Android code |
| `base/assets/flutter_assets/`         | Assets declared in `pubspec.yaml`, fonts, shaders                |
| `base/res/`                           | Android resources: launcher icons, `strings.xml`, themes         |
| `base/lib/<abi>/` (other `.so` files) | Native libraries from plugins or the NDK                         |

`<abi>` is a CPU architecture such as `arm64-v8a`. A bundle carries one `libapp.so` per architecture, and Google Play delivers each device only the one it needs.

### iOS

`flutter build ipa` produces an Xcode archive and an `.ipa`. Inside the app bundle (`Runner.app`):

| Path in the app                            | What it is                                                     |
| ------------------------------------------ | -------------------------------------------------------------- |
| `Frameworks/App.framework/App`             | Your Dart code, framework code and Dart packages, AOT-compiled |
| `Frameworks/Flutter.framework`             | The Flutter engine, including the Dart runtime                 |
| `Runner` (the app’s executable)            | Your Swift and Objective-C code, and plugins’ native code      |
| `Frameworks/App.framework/flutter_assets/` | Assets declared in `pubspec.yaml`, fonts, shaders              |
| `Assets.car`, `Info.plist`, …              | Xcode-managed resources: app icon, launch screen, settings     |

Plugins can also add their own frameworks under `Frameworks/`.

macOS apps use the same frameworks under `Contents/Frameworks/`. On Linux, the Dart code is `lib/libapp.so` in the build’s `bundle` directory, and on Windows it is `data/app.so` next to the app’s executable. Both keep assets in `data/flutter_assets`.

## What Code Push can update, and why

Shorebird’s engine is a fork of the Flutter engine with an updater built in. When your app starts, the engine decides which Dart code to run: the code in `libapp.so` or `App.framework` that shipped with the release, or a patch the updater downloaded earlier. That choice happens inside the engine, so any change that only touches compiled Dart code can ship as a patch:

* your app’s Dart code, including widgets, state management and business logic
* Dart packages from pub.dev, as long as they don’t include native code
* values from `--dart-define`, which are compiled into your Dart code

Everything else in the tables above is loaded by the operating system from the installed, store-signed app, not by the Dart VM. Shorebird doesn’t replace it, so it changes only with a new store release:

* **Java, Kotlin, Swift and Objective-C code**, yours and plugins’ (see [Plugins and native code](/flutter-concepts/plugins-and-native-code/))
* **The Flutter engine**, so you can’t change your app’s Flutter version with a patch
* **Assets and Android or iOS resources** (see [Assets in Flutter apps](/flutter-concepts/assets/))

`shorebird patch` compares the new build with the release, warns you when native code or assets changed, and only ships the Dart code. The [Is your change patchable?](/code-push/#is-your-change-patchable) and [What types of changes can be included in a patch?](/code-push/#what-types-of-changes-can-be-included-in-a-patch) sections of the Code Push overview summarize the rules.

How a patch runs differs by platform. On Android, macOS, Windows and Linux, a patch is a new AOT-compiled copy of your Dart code. On iOS, Apple only allows downloaded code that is interpreted, so Shorebird runs the functions a patch changed in an interpreter and everything else from the release’s `App.framework`. [System Architecture](/code-push/system-architecture/) explains the details, and [Patch Performance](/code-push/performance/) covers when that matters.

## App size

A release build’s size is mostly the engine, your compiled Dart code, and your assets. To see what takes the space, build with `--analyze-size` (`flutter build appbundle --analyze-size --target-platform android-arm64`, since size analysis takes one Android architecture, or `flutter build ios --analyze-size`) and open the generated `*-code-size-analysis_*.json` file in DevTools’ app size tool. Flutter’s [app size guide](https://docs.flutter.dev/perf/app-size) explains how to estimate the download size users actually see and how to reduce it.

Shorebird’s engine adds a few megabytes to the engine library; see [How big of a dependency footprint does this add?](/code-push/faq/#how-big-of-a-dependency-footprint-does-this-add) Patches are downloaded separately and stored in the app’s data directory, not added to the installed app (see [What does the Shorebird updater store on disk?](/code-push/faq/#what-does-the-shorebird-updater-store-on-disk)).

## Obfuscation and debug symbols

`--split-debug-info=<directory>` moves the debug information for your Dart code out of the binary into one symbols file per architecture (such as `app.android-arm64.symbols`), which makes the app smaller. `--obfuscate` also renames your classes and functions. Both are described in Flutter’s [Obfuscate Dart code](https://docs.flutter.dev/deployment/obfuscate) guide.

Stack traces from such a build are unreadable without the symbols file from the build that produced the code. With Shorebird, that means keeping the release’s symbols and each patch’s. See [Using obfuscation with Shorebird](/code-push/guides/security-tools/#using-obfuscation-with-shorebird) and [Reading crashes from patches](/code-push/crash-reporting/uploading-symbols/#reading-crashes-from-patches).
