Dart 90 🎯 Internal Architecture: Dart VM Architecture, Kernel AST, and IL Optimizations
The Dart VM is not a single interpreter. It is a collection of execution pipelines that share a common front end but diverge at the point where Dart code becomes machine code. The Common Front End (CFE), written in Dart, parses source files and produces a Kernel AST serialized into a .dill file. The VM never executes Dart source directly; it loads the Kernel binary lazily, compiles functions on first call through an unoptimizing compiler, and then—only in JIT mode—recompiles hot functions through an optimizing compiler that uses runtime type feedback to produce specialized machine code. The AOT pipeline replaces the feedback-driven optimizer with whole-program static analysis, because an AOT snapshot must contain executable code for every function that could be invoked and cannot rely on speculative assumptions that might be violated at runtime .
The architecture is a response to two conflicting requirements. JIT mode must support hot reload, incremental recompilation, and rich debugging, which means it must retain type information and execution profiles. AOT mode must start fast, run without a compiler, and survive on platforms where JIT is prohibited, which means it must perform all analysis ahead of time and produce code that reaches peak performance immediately with no warmup . The Kernel AST is the shared currency between the two pipelines, and the intermediate language (IL) is the representation that both compilers consume before lowering to machine code.
This chapter covers the VM’s execution modes and isolate model, the Kernel AST as the boundary between front end and back end, the unoptimizing and optimizing compiler pipelines, the IL instruction set and its lowering to machine code, and the deoptimization mechanism that lets optimized code fall back to unoptimized code when a speculative assumption fails. It assumes familiarity with Dart’s compilation modes and the distinction between JIT and AOT from the preceding chapters.
Key point: The VM never runs Dart source. The CFE produces a Kernel binary, the VM loads it lazily, and every function initially points to a LazyCompileStub. First invocation triggers the unoptimizing compiler, which builds a control flow graph of IL instructions and lowers it to machine code without optimizations. JIT mode then recompiles hot functions with an optimizing compiler that inlines, specializes, and speculates based on runtime feedback. AOT mode skips the feedback loop and uses type flow analysis to make conservative whole-program decisions. Deoptimization is the escape hatch that lets optimized code revert to unoptimized code when a speculation fails.
Why the VM architecture separates front end, back end, and runtime
The CFE is shared across tools. The Common Front End is written in Dart and used by the VM, dart2js, and the Dart Dev Compiler. Its job is to parse Dart source, perform type checking, apply Kernel-to-Kernel transformations, and serialize the result into a Kernel binary (.dill file) . The VM does not parse Dart source; it expects a Kernel binary. The standalone dart executable hides this by hosting a kernel service isolate that compiles source to Kernel on the fly, but Flutter separates the two: frontend_server runs on the developer machine and the Kernel binary is sent to the device .
Kernel loading is lazy. When the VM loads a Kernel binary, it does not deserialize the entire program. It loads basic information about libraries and classes, and each entity retains a pointer back to the binary so that more information can be loaded when needed. Class members are read when the class is used; function bodies are deserialized only when the function is invoked . This laziness is what makes startup fast for large applications that use a small fraction of their code on the initial screen.
The runtime is shared, the compilers are not. The object model, garbage collector, isolate model, and core library native methods are the same in JIT and AOT modes. The difference is the compilation pipeline: JIT has an unoptimizing compiler, an optimizing compiler, and a deoptimizer; AOT has a precompiler that runs whole-program analysis and produces a snapshot with code that cannot deoptimize. AOT snapshots include a code section that maps directly into the heap without deserialization, which is why startup is faster than JIT .
The trade-off. The shared front end means that language features must be expressible in Kernel AST, and the CFE must be updated for every language change. The lazy loading means that the first invocation of a function pays a compilation cost in JIT mode, which is the warmup that AOT eliminates. The AOT pipeline’s whole-program analysis is conservative, which means it may retain code that JIT would have discarded as unreachable, but it produces code that never needs to deoptimize because it never speculates.
a. Isolates, threads, and the execution environment
Every Dart program runs inside at least one isolate. An isolate has its own heap, its own mutator thread, and its own event loop. Isolates do not share mutable state; they communicate by sending messages through ports . The VM can run many isolates concurrently, but a single OS thread can only enter one isolate at a time, and an isolate has exactly one mutator thread that executes Dart code. The isolate may have additional helper threads: a background JIT compiler thread, a concurrent GC marker, and a GC sweeper .
The VM manages OS threads through a thread pool and a Task abstraction rather than spawning dedicated threads for every operation. The concurrent sweeper posts a ConcurrentSweeperTask to the pool, and the isolate message handler posts a MessageHandlerTask when a message arrives . This pooling is what allows the VM to scale to many isolates without a one-to-one thread mapping.
b. The Kernel AST and the unoptimizing compiler
The Kernel AST is the serialized representation of the program after parsing and type checking. The VM’s KernelLoader deserializes it into VM objects, but function bodies remain as serialized AST until the function is invoked . When a function is first called, it points to a LazyCompileStub that asks the runtime to generate executable code. The unoptimizing compiler then runs.
The unoptimizing compiler produces machine code in two passes. First, it walks the serialized AST for the function body and generates a control flow graph (CFG) of basic blocks filled with intermediate language (IL) instructions. The IL at this stage resembles a stack-based virtual machine: instructions take operands from a stack, perform an operation, and push the result . Second, the CFG is lowered to machine code with a one-to-many mapping: each IL instruction expands to multiple machine instructions. No optimizations are performed; the goal is to produce executable code quickly .
The unoptimizing compiler does not resolve calls that were not resolved in the Kernel binary. Calls are compiled as fully dynamic, and the VM uses inline caching to make them fast. Each call site has an ICData object that maps the receiver’s class to the method to invoke and tracks invocation frequency. A shared lookup stub checks the cache; if the receiver’s class matches an entry, it tail-calls the cached method and increments the frequency counter. If not, it invokes the runtime system to resolve the method, updates the cache, and subsequent invocations hit the fast path .
c. The optimizing compiler, IL optimizations, and deoptimization
The JIT optimizing compiler runs on functions that the runtime identifies as hot, based on the invocation counters in the inline caches. The optimizer uses the type feedback from those caches to make speculative assumptions: if a call site has only ever seen instances of Dog, the optimizer can inline Dog‘s method and check the receiver’s class before executing the inlined body. If a Cat arrives later, the check fails, and the optimized code deoptimizes back to the unoptimized version .
The optimizing compiler operates on the same IL but applies a series of transformations: inlining, dynamic type specialization, dead code elimination, and constant folding . The IL is converted to a static single assignment (SSA) form, optimized, and then lowered to machine code. The optimizer can also allocate objects on the stack when escape analysis proves they do not escape the function, and it can eliminate bounds checks when it proves the index is within range.
Deoptimization is the mechanism that makes speculative optimization safe. The optimizer emits deoptimization instructions alongside the optimized code. These instructions describe how to reconstruct the state of the unoptimized frame from the state of the optimized frame: which values are in which registers, which objects are on the stack, and where execution should resume in the unoptimized code . When a speculation fails—a type check fails, a null check fails, a deoptimization instruction is reached—the VM executes the deoptimization instructions, reconstructs the unoptimized frame, and continues execution in the unoptimized code. The --trace-deoptimization flag prints the cause and location of every deoptimization.
d. The AOT pipeline and the absence of deoptimization
AOT compilation cannot rely on runtime feedback because there is no runtime compilation phase and no deoptimization target. The AOT compiler must produce code for every function that could be invoked, and that code must not depend on speculative assumptions that could be violated . It performs type flow analysis (TFA) over the whole program, starting from a known set of entry points and tracing which classes are allocated, which methods are called, and how types flow through the program. The analysis is conservative: it errs on the side of correctness, retaining code and type checks that JIT might have optimized away, because AOT has no deoptimization escape hatch .
The AOT snapshot includes both the serialized heap (objects, class metadata, strings) and the machine code for every reachable function. The code section is mapped directly into memory without deserialization, which is why AOT startup is faster than JIT startup . The trade-off is peak performance: JIT has the best peak performance because it can specialize based on actual runtime types, while AOT has the best startup time and consistent performance from the first instruction.
Complete Example Session
// ============================================
// PART 1: SOURCE TO KERNEL AST
// ============================================
// The CFE parses this source and produces a .dill file.
// The VM never sees the source; it loads the Kernel binary.
//
// dart pkg/vm/bin/gen_kernel.dart --platform=vm_platform.dill -o hello.dill hello.dart
//
// The .dill contains serialized Kernel AST:
// - Libraries, classes, functions
// - Function signatures
// - Function bodies (serialized AST)
// ============================================
// PART 2: LAZY LOADING
// ============================================
// VM loads basic library/class info.
// Function bodies deserialized on first invocation.
// Each function initially points to LazyCompileStub.
// ============================================
// PART 3: FIRST INVOCATION — UNOPTIMIZING COMPILER
// ============================================
// LazyCompileStub triggers unoptimizing compilation.
// Pass 1: AST → CFG with IL instructions (stack-based).
// Pass 2: IL → machine code (one-to-many lowering).
// No optimizations. Goal: executable code quickly.
// ============================================
// PART 4: INLINE CACHING
// ============================================
// Dynamic calls use ICData per call site.
// First call: runtime resolution, cache update.
// Subsequent calls: fast path, frequency counter increment.
// ============================================
// PART 5: HOT FUNCTION — OPTIMIZING COMPILER (JIT)
// ============================================
// When invocation count crosses threshold, optimizer runs.
// Uses type feedback from ICData.
// Inlines, specializes, eliminates dead code.
// Emits deoptimization instructions alongside optimized code.
// ============================================
// PART 6: DEOPTIMIZATION
// ============================================
// Speculation: call site only sees Dog.
// Optimizer inlines Dog.bark(), checks class.
// Cat arrives → check fails → deoptimize.
// Deoptimization instructions reconstruct unoptimized frame.
// Execution continues in unoptimized code.
// ============================================
// PART 7: AOT PIPELINE
// ============================================
// No runtime feedback. No deoptimization.
// Type Flow Analysis from entry points.
// Conservative: errs on correctness.
// Produces snapshot with code for every reachable function.
// ============================================
// PART 8: SNAPSHOT STRUCTURE
// ============================================
// AOT snapshot: serialized heap + code section.
// Code section maps directly into memory.
// No deserialization for code.
// Startup is fast; peak performance is lower than JIT.
// ============================================
// PART 9: IL INSTRUCTION EXAMPLE
// ============================================
// Dart: a + b
// IL (stack-based):
// Push a
// Push b
// Add
// Return
//
// Optimized IL (SSA):
// v1 = Load a
// v2 = Load b
// v3 = Add v1, v2
// Return v3
// ============================================
// PART 10: THE COMPLETE FLOW
// ============================================
// 1. CFE parses source → Kernel AST → .dill
// 2. VM loads .dill lazily.
// 3. Function call → LazyCompileStub.
// 4. Unoptimizing compiler → executable code.
// 5. Hot function → optimizing compiler (JIT).
// 6. Speculation fails → deoptimization.
// 7. AOT: TFA → snapshot with code, no deopt.
Quick Reference
Execution Modes
| Mode | Compiler | Feedback | Deoptimization | Startup |
|---|---|---|---|---|
| JIT | Unoptimizing + optimizing | Yes | Yes | Slow (warmup) |
| AOT | Precompiler (TFA) | No | No | Fast |
| AppJIT | Snapshot of trained JIT | From training | Yes | Medium |
Kernel and IL
| Stage | Representation | Produced By | Consumed By |
|---|---|---|---|
| Front end | Kernel AST | CFE | VM loader |
| First compile | Stack-based IL | Unoptimizing compiler | Lowering |
| Optimized compile | SSA IL | Optimizing compiler | Machine code |
Inline Caching
| Component | Purpose |
|---|---|
ICData | Per-call-site class→method map, frequency counters |
| Lookup stub | Fast path check, tail-call cached method |
| Runtime resolution | Slow path, cache update |
Deoptimization
| Concept | Detail |
|---|---|
| Trigger | Speculation failure (type check, null check) |
| Mechanism | Deoptimization instructions reconstruct unoptimized frame |
| Trace flag | --trace-deoptimization |
| AOT | No deoptimization; conservative analysis |
Best Practices
✅ Do This:
// Understand that debug mode is JIT with warmup. // ✅
// Profile/release mode is AOT with fast startup.
// Use AOT for production where startup matters. // ✅
flutter build apk --release
// Expect JIT to reach higher peak performance. // ✅
// Long-running servers benefit from JIT warmup.
// Use --trace-deoptimization to diagnose performance. // ✅
// Frequent deopt indicates unstable types at a call site.
❌ Don’t Do This:
// Assume AOT and JIT have the same performance profile. // ❌
// JIT: best peak. AOT: best startup.
// Expect AOT to deoptimize. // ❌
// AOT code is conservative; no deopt target.
// Confuse Kernel AST with machine code. // ❌
// Kernel is serialized AST; IL is lowered to machine code.
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Slow startup in debug | JIT warmup | Profile/release uses AOT |
| Frequent deoptimizations | Polymorphic call sites | Stabilize types or accept |
| AOT binary larger than expected | Conservative TFA retains code | Tree shaking, tree-shaking flags |
| Confusing Kernel and IL | Both are intermediate | Kernel: AST; IL: stack/SSA for codegen |
Real-World Examples
1. Generate Kernel Binary
dart pkg/vm/bin/gen_kernel.dart --platform=vm_platform.dill -o hello.dill hello.dart
2. Dump Kernel AST
dart pkg/vm/bin/dump_kernel.dart hello.dill
3. Trace Deoptimization
dart --trace-deoptimization run hello.dart
4. AppJIT Snapshot
dart --snapshot-kind=app-jit --snapshot=app.jit hello.dart
5. AOT Snapshot (Dart CLI)
dart compile aot-snapshot hello.dart
6. Inline Cache Frequency
// ICData tracks how often each class was seen at a call site.
// Optimizer uses this to decide inlining.
7. Lazy Compile Stub
// Function bodies initially point to LazyCompileStub.
// First invocation triggers unoptimizing compilation.
8. AOT No Deopt
// AOT code has no deoptimization instructions.
// Type checks are explicit, not speculative.
9. SSA IL
// Optimizer converts stack-based IL to SSA.
// v1 = Load a; v2 = Load b; v3 = Add v1, v2;
10. Snapshot Code Section
// AOT snapshot code section maps directly into memory.
// No deserialization for code.
Visual: The VM Pipeline
┌──────────────────────────────────────────────┐
│ SOURCE │
│ │ │
│ ▼ │
│ CFE (Common Front End) │
│ parse, type check, transform │
│ │ │
│ ▼ │
│ Kernel AST (.dill) │
│ │ │
│ ▼ │
│ VM KernelLoader (lazy) │
│ │ │
│ ▼ │
│ Function call → LazyCompileStub │
│ │ │
│ ▼ │
│ Unoptimizing compiler │
│ AST → CFG (stack IL) → machine code │
│ │ │
│ ▼ │
│ Executable (unoptimized) │
│ │ │
│ ├── JIT: hot → optimizing compiler │
│ │ (SSA IL, inline, specialize) │
│ │ → optimized code │
│ │ → speculation fails → deopt │
│ │ │
│ └── AOT: TFA → snapshot with code │
│ (conservative, no deopt) │
│ │
└──────────────────────────────────────────────┘
Kernel AST vs IL
┌──────────────────────────────────────────────┐
│ KERNEL AST IL │
│ │
│ Serialized AST Instruction stream │
│ from CFE. for codegen. │
│ │
│ Libraries, classes, Stack-based (first) │
│ functions, bodies. SSA (optimized). │
│ │
│ Loaded lazily. Generated per │
│ Function bodies function on │
│ deserialized on call. compilation. │
│ │
│ Boundary between Boundary between │
│ front end and VM. compiler and │
│ machine code. │
│ │
└──────────────────────────────────────────────┘
JIT vs AOT Trade-off
┌──────────────────────────────────────────────┐
│ JIT AOT │
│ │
│ Runtime feedback. Static analysis. │
│ Speculative. Conservative. │
│ Deoptimization. No deopt. │
│ Warmup. No warmup. │
│ Best peak. Best startup. │
│ │
│ Hot function: Whole program: │
│ inline, specialize. TFA, snapshot. │
│ │
│ Debug/profile. Release. │
│ │
└──────────────────────────────────────────────┘
Deoptimization
┌──────────────────────────────────────────────┐
│ SPECULATION FAILS │
│ │
│ Optimized code: │
│ check receiver is Dog │
│ inline Dog.bark() │
│ │
│ Cat arrives: │
│ check fails │
│ │ │
│ ▼ │
│ Deoptimization instructions execute: │
│ reconstruct unoptimized frame │
│ resume in unoptimized code │
│ │ │
│ ▼ │
│ Correct behavior, lower performance. │
│ │
│ AOT has no deopt target. │
│ Type checks are explicit, not speculative. │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Front end | CFE, produces Kernel AST (.dill) |
| VM entry | Kernel binary, loaded lazily |
| First compilation | Unoptimizing compiler, stack-based IL |
| Hot compilation | Optimizing compiler (JIT), SSA IL |
| Deoptimization | Speculation failure → unoptimized frame |
| AOT analysis | Type flow analysis (TFA), conservative |
| AOT snapshot | Heap + code, no deserialization for code |
| Inline cache | ICData per call site, frequency counters |
| Isolate | Own heap, one mutator thread, helper threads |
| Peak performance | JIT (with warmup), AOT (no warmup) |
Key takeaways:
- The VM never executes Dart source. The CFE produces a Kernel binary, and the VM loads it lazily. Function bodies are deserialized only when invoked, and each function initially points to a
LazyCompileStub. - The unoptimizing compiler produces executable code quickly. It walks the serialized AST, builds a CFG of stack-based IL, and lowers each instruction to machine code with no optimizations. Inline caching makes dynamic calls fast without static resolution .
- The JIT optimizing compiler uses runtime feedback to speculate. Hot functions are recompiled with inlining, type specialization, and SSA-based optimizations. Deoptimization instructions allow the optimized code to fall back to unoptimized code when a speculation fails .
- AOT replaces feedback with whole-program analysis. Type flow analysis determines which code is reachable and how types flow. The analysis is conservative because there is no deoptimization target, but the resulting snapshot starts fast and performs consistently .
- Kernel AST and IL are different representations. Kernel is the serialized AST produced by the front end and consumed by the VM loader. IL is the instruction stream generated during compilation and lowered to machine code. Confusing the two obscures where the boundaries are.
- The isolate is the execution unit. Each isolate has its own heap and mutator thread, communicates by message passing, and may have helper threads for JIT compilation and garbage collection. The VM manages threads through a pool and a task abstraction .
Remember: The Dart VM is a set of execution pipelines that share a front end and a runtime. The CFE turns source into Kernel AST. The VM loads Kernel lazily, compiles functions on first call through the unoptimizing compiler, and recompiles hot functions through the optimizing compiler in JIT mode. The optimizer speculates based on runtime type feedback and emits deoptimization instructions so that a failed speculation can revert to unoptimized code. AOT mode skips the feedback loop and uses type flow analysis to produce a snapshot with code that cannot deoptimize, trading peak performance for startup speed and consistent execution. The Kernel AST is the boundary between front end and VM; the IL is the boundary between compiler and machine code; deoptimization is the escape hatch that makes JIT speculation safe.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!