How Does Dart Work as a Programming Language?

Dart is a programming language developed by Google that compiles your code in two different ways depending on the situation: a fast, iterative mode for development and an optimized native mode for production. This dual compilation approach, combined with an unusual memory model and a strict type system, is what makes Dart the foundation for frameworks like Flutter. Here’s how each piece fits together.

Two Compilation Modes: JIT and AOT

Most languages pick one compilation strategy. Dart uses two, and switches between them based on what you’re doing.

During development, Dart uses Just-In-Time (JIT) compilation. When you run a Dart program with the dart run command, the Dart virtual machine compiles your code on the fly as it executes. This means you don’t need a separate build step before running your program. JIT compilation also enables features like hot reload (more on that below), which makes the development cycle dramatically faster. JIT-compiled code can actually achieve faster peak performance than pre-compiled code if the runtime has good data about which code paths are used most frequently, because it can optimize specifically for real usage patterns.

For production, Dart switches to Ahead-Of-Time (AOT) compilation. This converts your source code directly into native machine code before the program ever runs. The result is a standalone executable that includes a small Dart runtime. Because the code is already compiled to machine code, the program starts faster and runs with predictable performance. There’s no warm-up period where the compiler is still figuring out optimizations.

There’s also an intermediate option called a JIT snapshot, which pre-processes your code during a “training run” so the VM doesn’t need to parse and compile frequently used classes and functions again. This gives you faster startup than raw JIT while still allowing runtime optimization.

Isolates Instead of Threads

If you’ve worked with languages like Java or C++, you’re used to threads that share memory. Dart takes a fundamentally different approach called isolates, based on the Actor model from computer science.

Every piece of Dart code runs inside an isolate. Each isolate has its own chunk of memory and its own event loop. Isolates never share state with each other. If you have a global variable called configuration and you spawn a new isolate, that isolate gets its own copy. Changing it in the spawned isolate has zero effect on the original.

Isolates communicate exclusively through message passing, using pairs of send and receive ports. When you send an object from one isolate to another, it’s copied into the receiving isolate’s memory. The only exception is immutable objects like strings, which can’t be changed anyway, so Dart just sends a reference instead of copying them for better performance.

This design eliminates an entire category of bugs. Race conditions, deadlocks, and corrupted shared state simply can’t happen because no two isolates ever touch the same mutable data. The tradeoff is that you need to think more deliberately about how your concurrent code communicates, but the result is far more predictable behavior.

How Garbage Collection Works

Dart’s virtual machine uses a generational garbage collector, which means it treats short-lived objects differently from long-lived ones. This matters because most objects in a typical program are temporary: a string built for a single calculation, a list used in one loop iteration, a widget rebuilt for one frame.

The “new generation” collector handles these short-lived objects using a parallel, stop-the-world approach based on Cheney’s algorithm. It pauses execution briefly, identifies which new objects are still in use, copies them to a clean memory space, and reclaims everything else. Because most new objects die young, this process is fast.

Objects that survive multiple collection cycles get promoted to the “old generation,” which uses a different strategy. The old generation collector works concurrently with your code, marking objects that are still reachable and then sweeping away (or compacting) the rest. Running concurrently means it causes fewer noticeable pauses, which is critical for applications like Flutter where smooth 60fps rendering depends on consistent frame timing.

Sound Null Safety

Dart’s type system enforces something called sound null safety, which means the language guarantees at compile time that you won’t accidentally try to use a value that doesn’t exist.

In Dart, types are non-nullable by default. If you declare a variable as a String, it must always contain an actual string value. It can never be null. If you want to allow null, you explicitly mark the type with a question mark (String?). This distinction is enforced by both the code analyzer and the compiler, so errors that would crash your program at runtime in other languages get caught before you ever deploy.

The “sound” part is the key guarantee. If the type system determines that a variable has a non-nullable type, it is impossible for that variable to evaluate to null at runtime. This isn’t just a lint warning or a best-practice suggestion. It’s a hard guarantee baked into the compiler.

Running Dart on the Web

Dart doesn’t only compile to native machine code. For web applications, Dart compiles your code into JavaScript using a tool called dart2js, or into WebAssembly using dart2wasm. Both tools transform your Dart source into formats that browsers can execute natively.

This means you can write Dart code once and target mobile, desktop, and web platforms from the same codebase. The compiler handles the translation, injecting the appropriate environment markers so your code can detect which compilation target it’s running on and adapt if needed.

How Dart Powers Flutter

Flutter is the most prominent consumer of Dart’s capabilities, and the two are tightly integrated. At the core of every Flutter application is the Flutter engine, written mostly in C++, which handles low-level tasks like graphics rendering, text layout, and file I/O. This engine includes a Dart runtime and is exposed to your Dart code through a library called dart:ui, which wraps the underlying C++ in Dart classes.

When the platform requests a new frame (triggered by a screen refresh signal, for example), Flutter’s render tree creates a composited scene and passes it through dart:ui to the GPU for rendering. Your Dart code that defines the visual layout is compiled to native code and uses Flutter’s rendering backend to paint each frame. This tight loop between Dart’s compiled code and the native rendering engine is what allows Flutter apps to hit smooth frame rates without a JavaScript bridge or interpreted layer slowing things down.

Hot Reload: Updating Code Without Restarting

One of Dart’s most practical features for developers is hot reload, which lets you change your code and see results in under a second without losing the current state of your application.

When you trigger a hot reload, the toolchain identifies which source files have changed since the last compilation. It recompiles only the affected libraries and their dependents into compact kernel files, then sends those files to the Dart VM running on your device. The VM reloads all affected libraries from the new kernel files. Crucially, it does not re-execute your app’s startup code or reset any stored state. Instead, the framework triggers a rebuild of the widget tree using the new code while preserving everything else.

This works because of JIT compilation. The VM can accept new code at runtime and swap in updated class definitions, including new fields and functions, without tearing down the running program. It’s one of the main reasons Dart was chosen as Flutter’s language: the ability to iterate on a UI in real time, seeing layout and styling changes instantly, collapses feedback loops that traditionally took 30 seconds or more into roughly one.

Performance Compared to JavaScript

When compiled to native code, Dart generally outperforms JavaScript in both speed and memory usage. In benchmark comparisons on tasks like binary tree manipulation, Dart executables completed work in about 711 milliseconds using around 81 MB of memory, while equivalent JavaScript code running on the Bun runtime took 924 milliseconds and consumed roughly 174 MB. That’s about 23% faster with less than half the memory footprint.

These numbers reflect AOT-compiled Dart, where the code is already in machine-native form. The gap comes largely from Dart’s compilation model and memory isolation: without shared mutable state and with predictable garbage collection, the runtime can make more aggressive optimizations. For web targets where Dart compiles to JavaScript or WebAssembly, performance depends more on the browser’s own engine.