Inside the Dart Toolchain
Welcome to Part 1 of the Dart Internals & Performance series. The earlier Dart series taught you the language — types, classes, async, generics. This series goes one level down: what the machine actually does with your code, and how to make it fast.
Before we can talk about JIT vs AOT, garbage collection, const optimization, or FFI, we need a shared map of the territory. So we start with one question:
When you type
dart runor hit the green play button in Flutter, what actually turns your.dartfiles into something a CPU executes?
Most developers never look inside this box. They don't need to — until an app janks, a build balloons, or hot reload mysteriously stops working. Then the box matters a lot. By the end of this post you'll have a mental model of the whole pipeline, and every later part will slot into it.
New to the series? This part is the foundation. Bookmark it — Parts 2–7 all reference the pipeline you're about to learn. We're teaching against Dart 3.12 (the current stable as of mid-2026).
The big idea: one language, two engines
Imagine a hybrid car with two engines under one hood. One is tuned for the racetrack — instant throttle response, you can retune it mid-lap. The other is tuned for the highway — no flexibility, but maximum fuel efficiency for the long haul. Same car, same fuel, two completely different ways of turning that fuel into motion, each picked for the situation.
Dart is that car. The "fuel" is your source code. The two engines are:
| Engine | Used when | Optimized for |
| --- | --- | --- |
| JIT (Just-In-Time) | Development (dart run, flutter run) | Fast iteration, hot reload, debugging |
| AOT (Ahead-Of-Time) | Production (flutter build, dart compile exe) | Startup speed, predictable performance, small footprint |
This dual nature is the reason Flutter feels the way it does: sub-second hot reload while you build, and fast, jank-free apps when you ship. We'll dedicate all of Part 2 to JIT vs AOT. For now, just hold the picture: same code, two engines, chosen by context.
The pipeline, end to end
Whichever engine runs, your code travels the same first few stops. Here's the whole journey:
your .dart files
│
▼
┌──────────────┐
│ Front end │ parse + type-check → "kernel" (.dill)
│ (CFE) │
└──────┬───────┘
│ Kernel AST (dill)
▼
┌─────────┐ ┌─────────────┐
│ JIT │ or │ AOT │
│ VM │ │ compiler │
└────┬────┘ └──────┬──────┘
│ │
interprets + machine code
compiles hot snapshot
code at runtime │
│ ▼
▼ runs on a small
Dart VM runtime Dart runtime
(GC, scheduler) (GC, scheduler)
Two things to notice immediately:
- The front end is shared. Both engines start from the same parsed, type-checked representation.
- The runtime is shared too. Even AOT code doesn't run "bare metal" — it runs on top of a compact Dart runtime that provides garbage collection, the async scheduler, and type checks. We'll meet that runtime throughout the series (GC is all of Part 3).
Let's walk each stop.
Stop 1: The Common Front End (CFE) and kernel
The first thing the toolchain does is parse your source and type-check it. This is handled by a component called the Common Front End (CFE) — "common" because every Dart backend (JIT VM, AOT compiler, dart2js, Wasm) shares it. Write the parser and type checker once; reuse it everywhere.
The CFE's output is not machine code and not your original text. It's an intermediate representation called kernel — a simplified, fully-resolved tree of your program, stored in .dill files.
Analogy: kernel is like a translated, annotated screenplay. The original script (your source) has shorthand, slang, and implicit stage directions. The screenplay handed to the crew has every role cast, every pronoun resolved, every implicit thing made explicit — so anyone downstream can shoot it without re-reading the novel.
What "lowering to kernel" does for you:
- Resolves every identifier (no more "which
x?"). - Desugars convenience syntax into a small core (e.g.
async/await,?., collection-if, default values all become simpler constructs). - Bakes in the results of type inference.
You can actually see it. From any Dart project:
# Produce the kernel (.dill) for a file
dart compile kernel bin/main.dart
# → writes bin/main.dill
The .dill is a binary, but the key idea is that the heavy work of understanding your code happens once, here. Every backend consumes this clean kernel rather than re-parsing source.
Why you care: when Flutter does a hot reload, it doesn't recompile your whole app. It re-runs the CFE to produce a new kernel for the changed libraries and sends that delta to the running VM. The shared kernel format is what makes incremental, sub-second reload possible. More in Part 2.
Stop 2a: The JIT path (development)
In development, the kernel is handed to the Dart VM, which runs your program right now without a separate compile step you wait on.
The VM doesn't blindly compile everything to machine code up front — that would be slow to start. Instead it works like a smart restaurant that watches what sells:
- It starts by interpreting kernel (and quickly compiling with an unoptimized baseline) — code runs immediately.
- It profiles as your program runs, counting how often each function executes and what types actually flow through it.
- When a function gets "hot" (called a lot), the optimizing JIT compiler kicks in and compiles it to fast machine code, using the observed types to make aggressive assumptions.
- If an assumption later turns out wrong (a new type shows up), it deoptimizes — falls back to the safe version — and may re-optimize later.
// During development, this function starts interpreted.
// Call it in a hot loop and the JIT will compile + specialize it
// for the types it actually sees (e.g. assuming `int`).
int square(num x) => (x * x).toInt();
void main() {
var total = 0;
for (var i = 0; i < 1000000; i++) {
total += square(i); // 'square' gets hot → JIT optimizes it
}
print(total);
}
This "observe, then optimize" approach is why the JIT can sometimes produce code as fast as AOT for steady-state work — it has runtime knowledge an ahead-of-time compiler can only guess at. The trade-offs (and why we still don't ship JIT) are Part 2.
Remember: the JIT VM gives you hot reload, a full debugger,
dart:developerhooks, and DevTools. That rich, observable, mutable runtime is the development experience — and it's a different binary from what your users run.
Stop 2b: The AOT path (production)
When you ship, flexibility stops mattering and startup speed + predictability take over. So the toolchain takes the same kernel and runs it through the AOT compiler, which translates all of your reachable code to native machine code ahead of time.
# Compile a standalone, self-contained native executable
dart compile exe bin/main.dart -o build/app
./build/app # starts instantly — no compilation at runtime
Flutter does the equivalent under the hood when you run flutter build apk / ipa: your Dart becomes an AOT snapshot baked into the app bundle.
The defining property of AOT code: there is no compiler in the shipped app. The machine code is already there. The app doesn't warm up, doesn't profile, doesn't deoptimize — it just runs. That's perfect for a mobile app a user opens for ten seconds: you can't afford a warm-up period.
The cost is the mirror image of JIT's benefit: no hot reload in a release build, larger binaries (all that machine code), and the compiler has to be conservative because it never gets to observe real runtime types. Part 2 weighs all of this.
Stop 3: Snapshots — Dart's "save state"
We keep saying "snapshot." It's one of the most important and least-understood pieces, so let's pin it down.
A snapshot is a serialized image of Dart program state — code and/or objects — written to a file so it can be loaded later without redoing work.
Think of a video game save file. Instead of replaying the first ten hours every time you boot the game, you load a save and you're instantly back where you were. A Dart snapshot is the same trick: instead of re-parsing and re-compiling on every launch, the toolchain saves a ready-to-load image.
There are a few flavors worth knowing:
| Snapshot kind | Contains | Used for |
| --- | --- | --- |
| Kernel snapshot (.dill) | Kernel AST (not machine code) | Feeding the VM / incremental reload |
| AOT snapshot | Native machine code + the heap of pre-built objects | Production apps (flutter build, dart compile exe/aot-snapshot) |
| App-JIT / training snapshot | Optimized code gathered from a "training" run | Faster startup for JIT tools (e.g. CLI tools) |
The AOT snapshot is the star for Flutter. Crucially, it includes not just code but a pre-initialized heap — objects that can be created at compile time (hello, const!) are built once during compilation and frozen into the snapshot, so the running app doesn't allocate them at all. That's a direct bridge to Part 4, where we'll see how const feeds this.
# Just the AOT machine-code snapshot (loaded by a runtime), not a full exe
dart compile aot-snapshot bin/main.dart -o build/app.aot
dartaotruntime build/app.aot
Stop 4: The runtime — code never runs alone
Here's the misconception worth killing early: "AOT means it compiles to native code like C, so there's no runtime." Not true. Even fully AOT-compiled Dart runs on top of a small, embedded Dart runtime that every Dart program needs. It provides:
- The garbage collector — automatic memory management (all of Part 3).
- The async event loop / scheduler — the microtask and event queues from the async series.
- Type system enforcement — runtime type checks,
is/as, covariance checks. - Isolate management — spawning, message passing, per-isolate heaps.
- Core library implementations — the native bits behind
dart:core,dart:io, etc.
Analogy: your compiled code is the application; the runtime is the operating system it lives in. Even a native executable relies on an OS for memory, scheduling, and I/O. Dart's runtime is that thin OS layer that ships inside your app.
This is why a "Hello World" Dart executable is a few megabytes, not a few kilobytes: it bundles the runtime. And it's why understanding GC, isolates, and the type system (all "runtime" concerns) directly affects your app's performance — they're not free abstractions, they're code running alongside yours.
Putting it together: trace one program
Let's trace a tiny program through the whole pipeline, twice.
void main() {
const greeting = 'Hello, internals!';
print(greeting);
}
In development (dart run main.dart):
- CFE parses + type-checks → kernel (
.dill). - The VM loads kernel and starts interpreting immediately — you see output in milliseconds.
mainruns once, so it never gets "hot" enough to JIT-optimize. (const greetingwas already folded to a constant by the CFE — Part 4.)- The shared runtime handles the actual
print(an I/O call) and will GC the strings afterward.
In production (dart compile exe main.dart):
- CFE parses + type-checks → kernel.
- The AOT compiler turns all reachable code into machine code;
const greetingis pre-built into the snapshot's heap. - The result is an executable with the machine code and the embedded runtime.
- Running it: instant start, no compilation, runtime still present for GC/scheduling/I/O.
Same five lines. Same kernel. Two engines, two outputs, one shared runtime.
The tools you'll actually type
A quick reference for the commands behind all this — you'll use these throughout the series:
dart run bin/main.dart # JIT: run via the VM (dev)
dart compile exe bin/main.dart # AOT: self-contained native executable
dart compile aot-snapshot ... # AOT: machine-code snapshot (+ dartaotruntime)
dart compile kernel ... # just the .dill kernel
dart compile js ... # compile to JavaScript (web)
dart compile wasm ... # compile to WebAssembly (web)
flutter run # JIT debug build (hot reload!)
flutter run --release # AOT build on device (no hot reload)
flutter build apk/ipa # AOT production bundle
Key rule:
flutter run(debug) = JIT = hot reload.flutter run --release/flutter build= AOT = no hot reload but production speed. If hot reload "isn't working," the first thing to check is whether you're in a release build.
Practice Challenges
Challenge 1 — Name the engine. For each, say whether it uses JIT or AOT: (a) flutter run while developing, (b) the App Store build a user downloads, (c) dart compile exe, (d) hitting "hot reload."
Show solution
(a) JIT — debug builds run on the VM. (b) AOT — release builds are ahead-of-time compiled. (c) AOT — compile exe produces native code. (d) JIT — hot reload only exists in the JIT/debug build; it sends a new kernel delta to the running VM.
Challenge 2 — What is kernel? A teammate says "Dart compiles straight from source text to machine code." Correct them in one sentence.
Show solution
Both backends first lower source to kernel (.dill) via the shared Common Front End — a parsed, type-checked, desugared intermediate representation — and then the JIT VM or AOT compiler turns kernel into machine code. The CFE is shared; the backend differs.
Challenge 3 — Produce a snapshot. Write the commands to (a) generate the kernel .dill for bin/main.dart, and (b) build a self-contained native executable.
Show solution
dart compile kernel bin/main.dart # (a) → bin/main.dill
dart compile exe bin/main.dart -o app # (b) → ./app native executable
Challenge 4 — Bust the myth. True or false: "Because release Flutter apps are AOT-compiled to native code, they don't have a garbage collector or runtime." Defend your answer.
Show solution
False. AOT compiles your code to native machine code, but the program still runs on top of the embedded Dart runtime, which provides the garbage collector, async scheduler, type checks, and isolate management. "AOT" describes how your code is compiled, not the absence of a runtime.
Challenge 5 — Trace it. In development, you call a small function inside a loop a million times. Describe what the VM does with that function over the loop's lifetime.
Show solution
It starts interpreted / baseline-compiled so it runs immediately. As call counts climb, the function becomes hot, and the optimizing JIT compiles it to specialized machine code based on the types actually observed. If a later call violates an assumption, the VM deoptimizes to the safe version, possibly re-optimizing afterward.
Questions to test yourself
Q1 (basic). What does the Common Front End (CFE) produce, and why is it called "common"?
Show answer
It produces kernel (.dill) — a parsed, type-checked, desugared intermediate representation. It's "common" because every Dart backend (JIT VM, AOT compiler, dart2js, Wasm) shares the same front end instead of each re-implementing parsing and type checking.
Q2 (basic). Which build uses JIT and which uses AOT: flutter run vs flutter build apk?
Show answer
flutter run (debug) uses JIT (hence hot reload). flutter build apk produces a release build that is AOT-compiled.
Q3 (intermediate). What is a snapshot, and what does an AOT snapshot contain that a kernel snapshot doesn't?
Show answer
A snapshot is a serialized image of program state saved to load quickly later. A kernel snapshot (.dill) holds the kernel AST (no machine code). An AOT snapshot holds native machine code plus a pre-initialized heap of objects (including compile-time constants), so the app starts without compiling or rebuilding those objects.
Q4 (intermediate). Why does even an AOT-compiled Dart program still need a runtime?
Show answer
Because the runtime provides services your compiled code depends on: garbage collection, the async event-loop scheduler, runtime type checks, isolate management, and core-library native implementations. AOT compiles your code to native instructions but doesn't eliminate the need for these services — the runtime ships embedded in the app.
Q5 (advanced). How does the shared kernel format make Flutter's hot reload possible?
Show answer
On hot reload, Flutter re-runs the CFE on only the changed libraries to produce a new kernel delta (.dill), then injects it into the already-running VM, which swaps in the new code while preserving app state. Because the expensive parse/type-check work targets the compact, incrementally-producible kernel — not a full native recompile — the round trip stays sub-second.
Q6 (advanced). Explain why the JIT can sometimes match or beat AOT on steady-state throughput, despite AOT having "all the time in the world" to compile.
Show answer
The JIT has information AOT can never have: real runtime behavior. It observes which types actually flow through a function and how often it's called, then compiles speculatively-specialized machine code (e.g. assuming int, inlining the common path) and deoptimizes only if reality diverges. AOT must compile before any execution, so it stays conservative — it can't specialize on types it hasn't seen. For long-running, hot code paths, the JIT's profile-guided specialization can win; AOT wins on startup and predictability instead.
Wrapping up
You now have the map the rest of this series hangs on:
- Dart is one language with two engines — JIT for development, AOT for production.
- Both start from a shared Common Front End that lowers source to kernel (
.dill). - JIT interprets, profiles, and optimizes hot code at runtime (and can deoptimize); AOT compiles everything up front into a snapshot.
- A snapshot is a saved image of code and/or objects; the AOT snapshot bakes in a pre-initialized heap (constants!).
- Code never runs alone — a small runtime (GC, scheduler, type checks, isolates) is always present, even in AOT.
In Part 2 we put the two engines head-to-head: JIT vs AOT — exactly what each optimizes, why hot reload is a JIT-only superpower, and how to reason about startup time, binary size, and "warm-up" in your own apps.