Isolates & compute
The jank you can’t await away
Section titled “The jank you can’t await away”async/await fixes waiting (network, disk) — but it does not help with CPU-heavy work. Parsing a 5MB JSON, resizing an image, or running a crypto hash is synchronous computation, and it runs on the UI isolate. While it runs, the event loop can’t render frames, and the app freezes.
You cannot await your way out of this, because there is nothing to wait for — the CPU is busy. The fix is to move the work to a different isolate so the UI isolate stays free to render.
compute(): the common case
Section titled “compute(): the common case”For the everyday “run this expensive pure function off the main isolate,” Flutter gives you compute(). Pass it a top-level (or static) function and its argument; it spins up a background isolate, runs the function there, and returns the result as a Future.
// A top-level function — required, because it must run on another isolate.List<Item> parseItems(String jsonStr) { final raw = jsonDecode(jsonStr) as List; return raw.map((e) => Item.fromJson(e)).toList(); // heavy work}
// On the UI isolate — offload it, and await the result without janking:final items = await compute(parseItems, hugeJsonString);The UI keeps rendering at 60fps while the parse runs elsewhere. compute is the right tool ~90% of the time.
Raw isolates and ports
Section titled “Raw isolates and ports”For long-lived workers or two-way communication, use Isolate.spawn with ports — isolates don’t share memory, so they talk by sending messages over SendPort/ReceivePort.
flowchart LR main["UI isolate (keeps rendering)"] -->|send message| worker["Worker isolate (does CPU work)"] worker -->|send result back| main note["No shared memory: only copied messages cross"] --- worker
The boundary: no shared memory
Section titled “The boundary: no shared memory”The rule that shapes everything: isolates do not share memory. Anything you send is copied across the boundary (with some fast-path exceptions like TransferableTypedData). Consequences:
- You can’t pass a closure that captures UI state, or a live object graph you keep mutating.
- Sending very large data has a copy cost — sometimes the copy eats the savings.
- The worker function must be self-contained (top-level/static), taking plain data in and returning plain data out.
This is the same message-passing model from the Dart async lesson, now used for parallelism instead of concurrency.