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.
  • await on 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();
}
MechanismStarted byBest for
Isolate.runPassing a closureOne-off CPU work with a single result
Isolate.spawnA top-level or static function plus an initial messageLong-lived workers and pipelines
SendPort / ReceivePortSending a port to the other isolateEvery message that crosses the boundary
compute()Flutter's helper over Isolate.spawnThe same job in a Flutter app
TransferableTypedDataWrapping bytes before sendingLarge 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.0
dart 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 targetOutputNotes
dart runNothing on diskJIT: fastest edit-run cycle
dart compile exeNative executableAOT, includes the runtime, no Dart needed on the target
dart compile aot-snapshot.aot fileSmaller, but needs dartaotruntime present
dart compile jit-snapshot.dill fileSkips recompiling unchanged code in dev
dart compile jsJavaScriptTree-shaken output for browsers
⚠️
Never call a synchronous CPU-heavy function from an event handler. It blocks timers, I/O callbacks and the UI for its whole duration, and the delay grows with the input. Move it to Isolate.run or restructure it as chunked asynchronous work.

FAQ

Do isolates share objects?
No. Plain objects are copied, and objects that cannot be copied (a 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?
A command line tool or server ships as 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.

Classes, futures and streams Dart syntax and null safety

Last refreshed 2026-09-18.