Isolates, compute() & Concurrency
When Single-Threaded Isn't Enough
Open interactive version (quiz + challenge)Real-world analogy
Dart's single thread is like one chef in a kitchen. For most meals (UI tasks), one chef is fine. But for a huge banquet (heavy computation like image processing), you need to hire a temporary chef (Isolate) who works in their OWN kitchen with their OWN ingredients — they can't touch the main chef's stuff. When done, they send the result back via a window (SendPort).
What is it?
Isolates are Dart's concurrency primitive — separate execution threads with their own memory that communicate via message passing. compute() and Isolate.run() provide simple APIs for offloading CPU-heavy work. They prevent UI jank by keeping heavy computation off the main thread, while the event loop handles I/O-bound async work.
Real-world relevance
In an offline-first field operations app processing thousands of survey records, syncing with a remote server while the user continues working requires an isolate. The main thread keeps the UI responsive while a worker isolate crunches the sync logic, resolves conflicts, and sends progress updates back via a SendPort.
Key points
- Why Isolates Exist — Dart is single-threaded. Heavy computation (JSON parsing 10MB, image processing, encryption) blocks the event loop and causes UI jank — dropped frames, frozen scrolling. Isolates run code on a separate thread with their own memory, keeping the UI smooth.
- Isolate vs Thread — Isolates are NOT threads. Threads share memory (dangerous: race conditions). Isolates have completely separate memory (safe: no shared state). They communicate only through message passing (SendPort/ReceivePort). Interview: Why are Isolates safer than threads?
- compute() — Simple Isolate — Flutter's compute(function, data) runs a top-level or static function in an Isolate and returns the result. Perfect for one-shot heavy work. Limitation: the function must be top-level or static (not a closure that captures state).
- Isolate.spawn — Full Control — Isolate.spawn() creates an isolate with full control. You manage SendPort/ReceivePort for bidirectional communication. Use for long-running background work that needs ongoing communication, like a background sync service.
- When NOT to Use Isolates — Isolates have overhead: spawning time + serialization cost for messages. Don't use them for simple async work (API calls, file reads) — those are I/O bound, not CPU bound. The event loop handles I/O fine. Use isolates ONLY for CPU-heavy work.
- Isolate.run() — Dart 2.19+ — Isolate.run() is the modern replacement for compute(). Simpler API, same concept. Runs a function in a new isolate and returns the result. Preferred in pure Dart code (compute() is Flutter-specific).
- Message Passing & Serialization — Isolates communicate by sending messages through ports. Messages must be serializable (primitives, lists, maps, SendPort, TransferableTypedData). You can't send arbitrary objects with methods. This is a common gotcha.
- Practical Use Cases — JSON decoding large payloads, image compression/resizing, encryption/hashing, complex data transformations, search/filter on large datasets, PDF generation, database migrations. If it takes >16ms (one frame), consider an isolate.
- Worker Isolate Pattern — For repeated heavy tasks, create a long-running worker isolate that stays alive. Send it tasks via a SendPort, receive results back. This avoids repeated spawn overhead. Used in production for background sync engines.
- IsolatePool and Packages — For managing multiple isolates, consider packages like worker_manager or the upcoming Dart isolate groups. In production, you rarely need more than 1-2 worker isolates. Don't over-isolate — the overhead isn't free.
Code example
// Isolates & Concurrency — Interview Essentials
import 'dart:isolate';
import 'package:flutter/foundation.dart';
// 1. compute() — simplest approach (Flutter)
Future<List<Map<String, dynamic>>> heavyJsonParse(
String rawJson,
) async {
// Runs in a separate isolate, returns the result
return await compute(_parseJson, rawJson);
}
// Must be top-level or static — NOT a closure
List<Map<String, dynamic>> _parseJson(String raw) {
// This runs in a separate isolate
// Heavy parsing happens here without blocking UI
return jsonDecode(raw) as List<Map<String, dynamic>>;
}
// 2. Isolate.run() — modern Dart 2.19+
Future<int> computeHash(String data) async {
return await Isolate.run(() {
// Heavy computation in separate isolate
var hash = 0;
for (var i = 0; i < data.length; i++) {
hash = (hash * 31 + data.codeUnitAt(i)) & 0xFFFFFFFF;
}
return hash;
});
}
// 3. Full Isolate with bidirectional communication
Future<void> workerIsolateExample() async {
final receivePort = ReceivePort();
// Spawn isolate with our receive port
await Isolate.spawn(
_workerEntryPoint,
receivePort.sendPort,
);
// Listen for messages from the worker
await for (final message in receivePort) {
if (message is SendPort) {
// Worker sent us its SendPort — now we can talk to it
message.send('Process this data');
} else if (message == 'done') {
receivePort.close();
break;
} else {
print('Worker result: $message');
}
}
}
void _workerEntryPoint(SendPort mainSendPort) {
final workerReceivePort = ReceivePort();
// Send our port so main can talk back
mainSendPort.send(workerReceivePort.sendPort);
workerReceivePort.listen((message) {
// Process the work
final result = 'Processed: $message';
mainSendPort.send(result);
mainSendPort.send('done');
workerReceivePort.close();
});
}
// WHEN TO USE vs NOT USE:
// USE Isolates: NOT Isolates:
// - JSON parse 1MB+ - API calls (I/O bound)
// - Image resize - File reads (I/O bound)
// - Encryption - Simple state updates
// - Complex sort/filter - Timer/periodic tasks
// - PDF generation - Database queries (I/O)Line-by-line walkthrough
- 1. Using compute() to run heavy JSON parsing off the main thread
- 2. The function must be top-level — this is the isolate entry point
- 3. Inside the isolate: heavy work happens here without blocking UI
- 4. Isolate.run() — modern, cleaner API for one-shot isolate tasks
- 5. The closure runs in a completely separate memory space
- 6. Full isolate setup: create a ReceivePort to get messages
- 7. Spawn an isolate with an entry point function and our SendPort
- 8. Listen for messages from the worker isolate
- 9. The worker sends us its SendPort so communication is bidirectional
- 10. Worker entry point: receives the main thread's SendPort
- 11. Creates its own ReceivePort for incoming messages
- 12. Sends results back to the main isolate via message passing
Spot the bug
// In a StatefulWidget
void processData() async {
final result = await Isolate.run(() {
final data = widget.largeDataList; // Accessing widget state
return data.where((x) => x > 100).toList();
});
setState(() => processed = result);
}Need a hint?
Can an isolate access the parent's memory and objects?
Show answer
Isolates have separate memory — you cannot access 'widget.largeDataList' from inside an isolate closure. The data must be passed as a parameter. Fix: capture the data before the isolate call: 'final list = widget.largeDataList; final result = await Isolate.run(() => list.where((x) => x > 100).toList());' — Dart will serialize 'list' to the new isolate.
Explain like I'm 5
Imagine you're coloring a really big picture. If you try to color the whole thing yourself, you can't talk to your friends or play until it's done. But what if you tear off a section and give it to a friend? They color their part in their OWN room with their OWN crayons (isolate with separate memory). When they finish, they slide the colored section back under the door (message passing). Now you kept playing while they colored!
Fun fact
The name 'Isolate' comes from the concept of isolation in concurrent programming. Unlike threads which share memory (and need locks, mutexes, and semaphores to avoid race conditions), Dart isolates are completely isolated from each other — making concurrent Dart code inherently safer than most other languages.
Hands-on challenge
Create a function that takes a large list of integers (100,000+) and finds all prime numbers using Isolate.run(). Compare the execution time with and without isolate usage. Observe UI jank in a Flutter app when running the same function on the main isolate.
More resources
- Dart Concurrency (Dart Official)
- Isolate API Reference (Dart Official)
- Flutter compute() (Flutter Official)
- Isolates and Event Loops (Dart Official)