Dart 1 🎯 Dart Runtime Engine, JIT/AOT Compilers, and Execution Architecture
Dart is a client-optimized programming language designed for building fast applications on any platform. What makes Dart distinctive is not just its syntax or type system, but its execution architecture: a runtime that can run code in fundamentally different ways depending on the target platform and the development phase. The same Dart code can be interpreted on the fly during development, compiled to native machine code for production, or translated to JavaScript for the browser. Understanding how the runtime engine, the JIT compiler, and the AOT compiler work together is the foundation for understanding everything else about Dart.
The previous chapters in this series have covered JavaScript, TypeScript, and the React ecosystem. This chapter begins the Dart series with the execution model that sits underneath the language. Dart is the language that powers Flutter, but its runtime architecture was designed independently and evolved alongside the language itself. The VM that runs Dart today is a different machine from the one that ran the first version of the language in 2011.
Key point: Dart has two primary compilation modes: JIT (Just-in-Time) and AOT (Ahead-of-Time). The JIT compiler runs during development and provides sub-second hot reload by compiling code incrementally as the program executes. The AOT compiler runs before deployment and produces native machine code with instant startup and consistent performance. The Dart VM includes both modes, and the choice between them is determined by the target platform and the development phase, not by the language itself.
Why the Execution Architecture Matters
A programming language is not just its syntax. It is also the machine that runs it. The execution architecture determines startup time, peak performance, memory usage, and the development workflow. Dart’s architecture is unusual because it supports two very different execution models within a single runtime.
The development workflow problem. During development, the developer changes code and wants to see the result immediately. A compiler that takes seconds to rebuild the entire application makes iteration painful. Dart’s JIT compiler solves this with incremental recompilation: it compiles only the functions that changed, and it can swap the compiled code into the running program without restarting. This is what makes hot reload possible .
The production problem. In production, startup time and consistent performance matter more than iteration speed. A user opening an app wants it to load instantly. A server handling requests cannot afford JIT warmup pauses. Dart’s AOT compiler solves this by compiling the entire application to native machine code before deployment. The compiled code starts instantly and runs at consistent speed from the first instruction .
The platform problem. Some platforms forbid JIT compilation. iOS, for example, does not allow applications to generate and execute code at runtime. Dart’s AOT compiler makes it possible to run Dart on these platforms by compiling everything ahead of time .
The trade-off. JIT compilation has access to runtime type information and execution profiles, which allows it to apply speculative optimizations that make the code faster. AOT compilation does not have this information, so it must make conservative assumptions. The result is that JIT-compiled code can have better peak performance, while AOT-compiled code has better startup time and consistent performance. Dart’s architecture provides both, and the choice depends on the phase and platform .
a. The Dart VM and Isolates
The Dart Virtual Machine is the runtime that executes Dart code. It is not a single program but a collection of components: a JIT compiler, an AOT compiler, a garbage collector, a set of core libraries, and the execution engine that coordinates them. The VM is embedded in different hosts depending on the platform. On mobile and desktop, Flutter embeds the VM. On the command line, the dart executable embeds the VM. In the browser, the Dart code is compiled to JavaScript and runs in the browser’s JavaScript engine, not the Dart VM .
The VM’s fundamental unit of execution is the isolate. An isolate is an independent worker with its own heap and its own event loop. Isolates do not share memory. They communicate only by sending messages through ports. This model is similar to the actor model in other languages: each isolate is a self-contained universe that processes events and sends messages to other isolates .
The relationship between isolates and operating system threads is subtle. An OS thread can enter an isolate, execute Dart code, and then leave. It can then enter a different isolate. The VM guarantees that only one thread executes Dart code in a given isolate at any moment. The isolate can also have auxiliary threads for background compilation, garbage collection, and other internal tasks, but these threads do not execute Dart code .
The isolate model has significant implications for concurrency. Because isolates do not share memory, there are no data races. Because they communicate by message passing, the developer must explicitly define the messages that cross isolate boundaries. This is stricter than the shared-memory model of Java or C++, but it eliminates an entire class of concurrency bugs. Dart’s async/await syntax and Future and Stream types are built on top of the isolate model, providing a high-level abstraction for asynchronous programming .
b. The JIT Compiler: Development Mode
The JIT compiler is the component that makes Dart’s development workflow fast. It compiles functions on demand, observes how the program executes, and recompiles hot code paths with aggressive optimizations. The result is a system that starts running quickly and gets faster as it runs.
The JIT pipeline has two compilers. The unoptimizing compiler produces machine code as quickly as possible. It walks the serialized AST of each function, builds a control flow graph, and lowers the intermediate language instructions directly to machine code. It performs no optimizations. Its goal is to get the program running, not to make it fast .
The unoptimized code collects execution profiles as it runs. Inline caches record the receiver types observed at each call site. Execution counters track how often each function and basic block runs. When a function’s counter reaches a threshold, the function is submitted to the optimizing compiler .
The optimizing compiler starts from the same AST but translates it into static single assignment (SSA) form. It then applies speculative specializations based on the collected type feedback: inlining, range analysis, type propagation, representation selection, and other classical optimizations. The resulting code is much faster than the unoptimized version, but it relies on assumptions about the types observed at runtime. If those assumptions are violated — for example, if a class that was never extended gains a subclass through dynamic code loading — the VM deoptimizes: it discards the optimized code and returns to the unoptimized version .
The JIT compiler runs in a background thread, so compilation does not block the application. When compilation completes, the background thread requests the mutator thread to enter a safepoint and attaches the optimized code to the function. The next call to the function uses the optimized version. For long-running loops, the VM can perform on-stack replacement (OSR): it swaps the unoptimized frame for an optimized frame while the function is still running .
The JIT mode is what enables hot reload. The frontend_server process compiles the changed libraries to a Kernel binary, and the VM replaces the compiled code in the running isolate without restarting the application. The developer sees the change immediately .
c. The AOT Compiler: Production Mode
The AOT compiler produces native machine code before the application runs. It is used for production deployments on mobile and desktop, and it is required on platforms that forbid JIT compilation.
The AOT pipeline begins with type flow analysis (TFA). The compiler analyzes the entire program to determine which functions are reachable from the known entry points, which classes are allocated, and how types flow through the code. These analyses are conservative: they err on the side of correctness. The compiler must compile code for every function that could be invoked, even if it probably will not be .
After TFA, the compiler compiles every reachable function to native machine code. It does not apply speculative optimizations, because there is no runtime profile to guide them. Instead, it uses the type flow information to specialize the code — devirtualizing calls where the receiver type is known, for example. The resulting code is not as fast as JIT-optimized code, but it starts instantly and performs consistently .
Once all functions are compiled, the VM takes a snapshot of the heap. The snapshot is a low-level serialization of the object graph, optimized for fast startup. When the application starts, the VM maps the snapshot into memory and the application is immediately ready to run. There is no parsing, no compiling, no warmup .
The AOT-compiled application runs on a precompiled runtime, a variant of the Dart VM that excludes the JIT compiler and dynamic code loading facilities. This reduces the size of the runtime and ensures that the application cannot generate or execute code at runtime, which is required on platforms like iOS .
AOT compilation is invoked through the dart compile command. The dart compile exe subcommand produces a native executable. The dart compile aot-snapshot subcommand produces an AOT module that can be run with the dartaotruntime executable .
Complete Example Session
This session demonstrates the JIT and AOT compilation modes on a simple Dart application.
// ============================================
// PART 1: THE APPLICATION
// ============================================
// hello.dart
void main() {
print('Hello, Developer!');
}
// ============================================
// PART 2: RUNNING WITH JIT
// ============================================
// dart run hello.dart
// Output: Hello, Developer!
// The JIT compiler parses and compiles the code on the fly.
// The program starts quickly but the code is unoptimized.
// ============================================
// PART 3: COMPILING WITH AOT
// ============================================
// dart compile exe hello.dart -o hello
// Output: Generated: /path/to/hello
// The AOT compiler produces a native executable.
// The executable includes the compiled code and a snapshot of the heap.
// ============================================
// PART 4: RUNNING THE AOT EXECUTABLE
// ============================================
// time ./hello
// Hello, Developer!
// real 0m0.012s
// The AOT-compiled application starts in 12 milliseconds.
// The JIT-compiled version takes longer to start because it must compile first.
// ============================================
// PART 5: THE JIT SNAPSHOT
// ============================================
// dart compile jit-snapshot hello.dart -o hello.jit
// Output: Compiling hello.dart to jit-snapshot file hello.jit.
// The JIT snapshot contains the parsed classes and compiled code
// from a training run. Starting from the snapshot skips parsing
// and compilation for the code that ran during training.
// ============================================
// PART 6: THE AOT SNAPSHOT
// ============================================
// dart compile aot-snapshot hello.dart -o hello.aot
// Output: Generated: /path/to/hello.aot
// The AOT snapshot is architecture-specific.
// It contains executable code for every reachable function.
// It is run with the dartaotruntime executable.
// ============================================
// PART 7: THE SNAPSHOT FORMAT
// ============================================
// A snapshot is a serialized object graph.
// The format is low-level and optimized for fast startup.
// It is essentially a list of objects to create and instructions
// on how to connect them together.
// The idea has roots in Smalltalk images.
// The VM uses clustered serialization.
// Initially snapshots did not include machine code.
// The AOT compiler added the capability to include code sections.
// ============================================
// PART 8: THE DEOPTIMIZATION MECHANISM
// ============================================
// The JIT compiler makes speculative assumptions.
// Example: a class C is never extended.
// The optimizing compiler uses this to propagate types.
// If C gains a subclass at runtime, the assumption is invalid.
// The runtime finds and discards optimized code that relied on it.
// Frames on the execution stack are marked for lazy deoptimization.
// They deoptimize when execution returns to them.
// ============================================
// PART 9: THE ISOLATE MODEL
// ============================================
// Isolates are independent workers with their own heap.
// They do not share memory.
// They communicate by message passing through ports.
// One OS thread executes Dart code in an isolate at a time.
// The isolate can have auxiliary threads for compilation and GC.
// ============================================
// PART 10: THE COMPILATION COMPARISON
// ============================================
// JIT:
// - Compiles during execution
// - Observes runtime behavior
// - Applies speculative optimizations
// - Can deoptimize when assumptions break
// - Best peak performance, slow startup
// - Enables hot reload
// AOT:
// - Compiles before execution
// - No runtime profile
// - Conservative optimizations
// - No deoptimization needed
// - Best startup time, consistent performance
// - Required on platforms that forbid JIT
The ten parts cover the application, running with JIT, compiling with AOT, running the AOT executable, the JIT snapshot, the AOT snapshot, the snapshot format, the deoptimization mechanism, the isolate model, and the compilation comparison.
Quick Reference
The Compilation Modes
| Mode | When | Startup | Peak Performance | Use Case |
|---|---|---|---|---|
| JIT | During execution | Slow | Best | Development, hot reload |
| AOT | Before execution | Instant | Good | Production, iOS |
The Compilation Commands
| Command | Output | Purpose |
|---|---|---|
dart run | None | Run with JIT |
dart compile exe | Native executable | AOT compile for production |
dart compile aot-snapshot | .aot file | AOT module |
dart compile jit-snapshot | .jit file | JIT module |
dart compile kernel | .dill file | Kernel binary |
The Snapshot Types
| Type | Contains | Architecture-Specific |
|---|---|---|
| Kernel | Serialized AST | No |
| JIT | Parsed classes, compiled code | Yes |
| AOT | Executable code, heap snapshot | Yes |
The Isolate Model
| Aspect | Behavior |
|---|---|
| Memory | Not shared between isolates |
| Communication | Message passing through ports |
| Threading | One mutator thread per isolate |
| Concurrency | Isolates run independently |
Best Practices
✅ Do This:
# Use dart run for development
dart run hello.dart # ✅
# Use dart compile exe for production
dart compile exe hello.dart -o hello # ✅
# Use AOT for iOS and other JIT-restricted platforms
dart compile aot-snapshot hello.dart # ✅
# Use isolates for CPU-intensive work
Isolate.spawn(heavyComputation, data) # ✅
❌ Don’t Do This:
# Don't use JIT for production on mobile
dart run hello.dart # slow startup, warmup pauses # ❌
# Don't share memory between isolates
# Isolates do not share memory # ❌
# Don't expect JIT on iOS
# iOS forbids JIT compilation # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Slow startup in production | Using JIT | Use AOT |
| iOS build fails | JIT not allowed | Use AOT |
| Hot reload not working | JIT not enabled | Use dart run |
| Memory not freed | Isolates hold references | Send messages, terminate isolates |
| Deoptimization pauses | Speculative assumptions broken | Accept or restructure code |
Real-World Examples
1. Run with JIT
dart run hello.dart
2. Compile to Executable
dart compile exe hello.dart
3. Run the Executable
./hello
4. Compile to AOT Snapshot
dart compile aot-snapshot hello.dart
5. Compile to JIT Snapshot
dart compile jit-snapshot hello.dart
6. Spawn an Isolate
Isolate.spawn(workerFunction, message);
7. Send a Message
SendPort.send(data);
8. Receive a Message
ReceivePort.listen((message) { });
9. AOT for iOS
flutter build ios --release
10. Hot Reload
flutter run
Visual
The JIT Pipeline
┌──────────────────────────────────────────────┐
│ JIT PIPELINE │
│ │
│ Source Code │
│ │ │
│ ▼ │
│ Kernel AST (dill file) │
│ │ │
│ ▼ │
│ Unoptimizing Compiler │
│ └─ Fast, no optimizations │
│ └─ Produces executable code │
│ │ │
│ ▼ │
│ Execution + Profile Collection │
│ └─ Inline caches │
│ └─ Execution counters │
│ │ │
│ ▼ │
│ Optimizing Compiler (background) │
│ └─ SSA form, speculative optimizations │
│ └─ Deoptimization support │
│ │ │
│ ▼ │
│ Optimized Code │
│ │
└──────────────────────────────────────────────┘
The AOT Pipeline
┌──────────────────────────────────────────────┐
│ AOT PIPELINE │
│ │
│ Source Code │
│ │ │
│ ▼ │
│ Kernel AST (dill file) │
│ │ │
│ ▼ │
│ Type Flow Analysis (TFA) │
│ └─ Conservative, global analysis │
│ └─ Determines reachable code │
│ │ │
│ ▼ │
│ Compile All Functions │
│ └─ No speculative optimizations │
│ └─ Use TFA for specialization │
│ │ │
│ ▼ │
│ Snapshot with Code │
│ └─ Heap serialization │
│ └─ Executable code section │
│ │ │
│ ▼ │
│ Precompiled Runtime │
│ └─ No JIT, no dynamic loading │
│ └─ Instant startup │
│ │
└──────────────────────────────────────────────┘
The Isolate Model
┌──────────────────────────────────────────────┐
│ ISOLATE MODEL │
│ │
│ Isolate A Isolate B │
│ ┌─────────┐ ┌─────────┐ │
│ │ Heap │ │ Heap │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ │ Message Passing │ │
│ └────────────────────────┘ │
│ │
│ No shared memory. │
│ Communication through ports. │
│ │
└──────────────────────────────────────────────┘
JIT vs AOT Comparison
┌──────────────────────────────────────────────┐
│ JIT vs AOT │
│ │
│ JIT: │
│ ├─ Startup: slow (warmup) │
│ ├─ Peak: best │
│ ├─ Profile: available │
│ ├─ Deoptimization: yes │
│ └─ Hot reload: yes │
│ │
│ AOT: │
│ ├─ Startup: instant │
│ ├─ Peak: good │
│ ├─ Profile: not available │
│ ├─ Deoptimization: no │
│ └─ Hot reload: no │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Runtime | Dart VM |
| JIT mode | Just-in-time, during execution |
| AOT mode | Ahead-of-time, before execution |
| JIT strength | Peak performance, hot reload |
| AOT strength | Instant startup, consistent performance |
| JIT weakness | Slow startup, warmup |
| AOT weakness | No speculative optimizations |
| Execution unit | Isolate |
| Isolate memory | Not shared |
| Isolate communication | Message passing |
| Snapshot | Serialized heap, fast startup |
| Deoptimization | JIT fallback when assumptions break |
Key takeaways:
- Dart has two compilation modes: JIT and AOT. The JIT compiler runs during development and provides sub-second hot reload by compiling code incrementally as the program executes. The AOT compiler runs before deployment and produces native machine code with instant startup and consistent performance .
- The JIT compiler observes runtime behavior and applies speculative optimizations. The unoptimizing compiler produces code quickly. As the code runs, it collects execution profiles through inline caches and counters. When a function becomes hot, the optimizing compiler recompiles it with aggressive optimizations based on the collected type feedback .
- Deoptimization is the JIT’s safety net. When speculative assumptions are violated — for example, a class that was never extended gains a subclass — the runtime discards the optimized code and falls back to the unoptimized version. Frames on the execution stack are marked for lazy deoptimization .
- The AOT compiler uses type flow analysis instead of runtime profiles. TFA is a global, conservative analysis that determines which functions are reachable and how types flow through the program. All reachable functions are compiled without speculative optimizations. The result starts instantly and performs consistently .
- AOT compilation is required on platforms that forbid JIT. iOS does not allow applications to generate and execute code at runtime. Dart’s AOT compiler makes it possible to run Dart on these platforms by compiling everything ahead of time .
- The fundamental unit of Dart execution is the isolate. An isolate is an independent worker with its own heap and event loop. Isolates do not share memory. They communicate only by sending messages through ports. This eliminates data races and enforces explicit message passing .
- Snapshots provide fast startup by serializing the heap. A snapshot is a low-level serialization of the object graph, optimized for fast startup. Instead of parsing source code and creating internal data structures, the VM maps the snapshot into memory and the application is immediately ready to run .
Remember: Dart’s execution architecture is built around two compilers. The JIT compiler makes development fast with hot reload and runtime profiling. The AOT compiler makes production fast with instant startup and consistent performance. The isolate model provides concurrency without shared memory. The snapshot format provides fast startup by skipping parsing and compilation. The choice between JIT and AOT is determined by the platform and the phase, not by the language. And the deoptimization mechanism ensures that the JIT compiler’s speculative optimizations never compromise correctness.
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!