Isolates, compilation and packaging
Why heavy work freezes the event loop, how to move it to another isolate, and the commands that turn a project into an executable.
One thread, one event loop
Each isolate runs on a single thread with two queues: microtasks, which drain completely before the loop continues, and events, which cover timers, I/O and messages. Long synchronous work therefore blocks every callback in the isolate.
void main() {
// Microtasks run before the next event, in submission order
scheduleMicrotask(() => print('microtask'));
print('synchronous');
Future<void>.delayed(Duration.zero, () => print('event'));
Future<void>.delayed(Duration.zero, () => print('later queued event'));
// This blocks the whole isolate: nothing above or below runs until it ends
var total = 0;
for (var i = 0; i < 50000000; i++) {
total += i;
}
print('total=$total');
}
// Output order: synchronous, microtask, total, event, later queued event- Timers and callbacks are events, not microtasks, so queued microtasks always get in first.
awaiton an already-completed Future still yields to the microtask queue, which is why ordering surprises people.- In Flutter, a long synchronous block keeps the UI from repainting even though the framework is entirely asynchronous.
Isolates for real parallelism
Isolates are independent workers with their own heap and event loop. They share no mutable state, so there is nothing to lock; they communicate only by sending messages over ports.
import 'dart:isolate';
// Dart 2.19+: run a closure in a new isolate and await its result
Future<int> heavySum() => Isolate.run(() {
var total = 0;
for (var i = 0; i < 100000000; i++) {
total += i;
}
return total;
});
// The long-lived form: spawn, hand back a port, keep exchanging messages
void worker(SendPort toMain) {
final inbox = ReceivePort();
toMain.send(inbox.sendPort); // step 1: give the main isolate a channel
inbox.listen((message) {
if (message == 'stop') {
inbox.close();
Isolate.exit(); // efficient shutdown
}
toMain.send('echo: $message');
});
}
Future<void> main() async {
print(await heavySum());
final fromWorker = ReceivePort();
await Isolate.spawn(worker, fromWorker.sendPort);
final channel = (await fromWorker.first) as SendPort;
channel.send('ping');
print(await fromWorker.first); // echo: ping
channel.send('stop');
fromWorker.close();
}| Mechanism | Started by | Best for |
|---|---|---|
Isolate.run | Passing a closure | One-off CPU work with a single result |
Isolate.spawn | A top-level or static function plus an initial message | Long-lived workers and pipelines |
SendPort / ReceivePort | Sending a port to the other isolate | Every message that crosses the boundary |
compute() | Flutter's helper over Isolate.spawn | The same job in a Flutter app |
TransferableTypedData | Wrapping bytes before sending | Large buffers, to avoid copying |
Messages are copied, with two exceptions: SendPort and TransferableTypedData move by reference. Sending a large object graph back and forth costs real time, so structure the API around small messages or bytes.
Packaging and compilation
# pubspec.yaml
name: my_tool
description: A small command line tool.
version: 0.1.0
environment:
sdk: ^3.5.0
dependencies:
http: ^1.2.0
dev_dependencies:
test: ^1.25.0
lints: ^4.0.0dart pub get # resolve pubspec.lock
dart pub add http # add and record a dependency
dart pub upgrade --major-versions # move constraints forward
dart analyze # static checks
dart format . # rewrite to canonical style
dart test # run the test/ directory
dart run bin/my_tool.dart # JIT, fast startup during development
dart compile exe bin/my_tool.dart -o build/my_tool # native AOT
dart compile js web/main.dart -o build/main.js # for the browser
dart pub publish --dry-run # check before publishing| Compile target | Output | Notes |
|---|---|---|
dart run | Nothing on disk | JIT: fastest edit-run cycle |
dart compile exe | Native executable | AOT, includes the runtime, no Dart needed on the target |
dart compile aot-snapshot | .aot file | Smaller, but needs dartaotruntime present |
dart compile jit-snapshot | .dill file | Skips recompiling unchanged code in dev |
dart compile js | JavaScript | Tree-shaken output for browsers |
Isolate.run or restructure it as chunked asynchronous work.FAQ
Do isolates share objects?
ReceivePort, an open socket, a closure capturing non-sendable state) throw at send time. Ports and transferable byte buffers are the two things that pass by reference.Which compile target should I ship?
dart compile exe: one file, no SDK on the machine. Web code ships as JavaScript from dart compile js. Keep dart run for development, where startup time matters more than throughput.Related
Classes, futures and streams Dart syntax and null safety
Last refreshed 2026-09-18.