Isolate Is Not a Thread — From “It Works” to Truly Understanding
Unpacking the evolution of Dart’s concurrency model — from Slava Egorov’s talk to Isolate.run / NativeCallable in practice
I share Dart Isolate with the community roughly every year.
This post is a compilation of content I planned to share at the end of last year (I accidentally forgot, haha).
I’ll also include a new, complete explanation based on recent updates from the official team.
Preface: Common Misconceptions About Isolates#
Many Flutter developers have had a similar experience: hit a performance bottleneck in a project, Googled and found the advice “use Isolates to fix it,” followed the example and wrote Isolate.spawn(), and the problem was indeed solved — but with little understanding of the underlying principles.
This is a very common state: can use it, but don’t understand it.
Slava Egorov’s talk was made for exactly this. He’s a core Dart team member, previously worked on Chrome’s V8 engine, now leads Dart VM architecture design. Listening to him explain Isolate is like listening to a chef explain their dish — finally understanding why this ingredient, in which direction to take the seasoning.
This article summarizes the essence of this talk along with related writing I’ve published, providing a comprehensive explanation of Isolate with practical development perspectives.
1. Core Concept: The Nature of an Isolate#
Slava Egorov opened with a bombshell: An Isolate is not a Thread.
This goes against the intuition of most developers. Many have always understood Isolates as “Dart’s version of Threads,” but understanding this distinction is the key to mastering Dart’s concurrency model.
An Isolate Is a Space, Not an Actor#
Use an analogy to understand: imagine an office. A Thread is an employee working inside the office, while an Isolate is the office itself — containing desks, files, computers, and all other resources. The employee enters the office to work, uses the resources inside, and leaves when the task is done.
This mental model shift is crucial:
Isolate:
- Nature — Memory Space / Container
- Agency — Passive (entered into)
- Memory — Owns an independent Heap
- State Isolation — Fully Isolated
Thread:
- Nature — Execution Unit
- Agency — Active (executes)
- Memory — Shared process memory
- State Isolation — Shared State
Why Does This Matter?#
Once you understand this mental model, many common confusions are resolved.
For example, why can’t Isolates directly share objects? Because they are different rooms, each with its own things. You can’t just grab documents from the office next door — you have to make a copy and send it over.
Or, why does an Isolate need “startup time”? Because you’re setting up an entirely new office, not just hiring a new employee.
Execution Model#
An Isolate itself does not execute any operations; it is simply a container that holds objects and static state. The actual code execution is done by the operating system’s Native Threads:
- A Thread enters the Isolate
- Executes code within that Isolate’s memory space
- Modifies state
- Exits the Isolate when done
This is like you (a Thread) walking into a meeting room (an Isolate), using the whiteboard and projector to complete your work, and then leaving. The items in the meeting room remain, but you can go to another meeting room to do something else.
2. Isolate Club Rules#
Slava Egorov used a very interesting analogy: Club Rules.
Each Isolate is like an exclusive private club with strict entry rules:
This Explains the “Single-Threaded” Claim#
You may have heard the claim that “Dart is a single-threaded language.” Strictly speaking, this isn’t quite accurate — but within a single Isolate, code execution is indeed serialized.
// Why is Dart called a "single-threaded" language?
// Because within a single Isolate, code execution is indeed serialized
void main() {
// These operations execute sequentially within the same Isolate
doTask1(); // Executing, no other Thread can enter
doTask2(); // Executes after Task1 completes
doTask3(); // Executes after Task2 completes
}
Key Insight: Dart’s “single-threaded” behavior is a consequence of Isolate isolation rules, not a limitation of the language itself.
Why Is It Designed This Way?#
At first glance, this restriction seems unreasonable — why not allow multiple Threads to enter an Isolate simultaneously? Wouldn’t that be faster?
But this is precisely where Dart aims to protect developers. In traditional multithreaded programming, the most frightening bugs are Race Conditions — two threads modifying the same variable simultaneously, producing unpredictable results. These bugs are extremely hard to debug because they don’t happen every time.
Dart’s design philosophy is: rather than letting developers handle complex synchronization issues themselves, prevent the problem at the architectural level.
This is particularly valuable in complex state management scenarios. Within an Isolate, you never need to worry about “will this variable be modified by another thread?” — because it simply cannot happen.
3. Communication Mechanism: Message Passing#
Since Isolates are completely isolated from each other, how do they communicate?
The answer is: Message Passing.
SendPort and ReceivePort: Isolate Communication Channels#
Think of it like a mailroom system between offices. Want to send a document to another department? Put it in the outbox (SendPort), and the other side picks it up from the inbox (ReceivePort).
Here’s a complete example:
import 'dart:isolate';
import 'dart:async';
// Worker Isolate entry point
void workerIsolate(SendPort mainSendPort) {
final workerReceivePort = ReceivePort();
// 1. Send the Worker's SendPort to the Main Isolate
mainSendPort.send(workerReceivePort.sendPort);
// 2. Listen for messages from the Main Isolate
workerReceivePort.listen((message) {
print('[Worker] Received: "$message"');
if (message is String && message.startsWith('task:')) {
final taskData = message.substring(5);
final result = taskData.toUpperCase();
// 3. Send the result back to the Main Isolate
mainSendPort.send('result:$result');
}
});
}
void main() async {
final mainReceivePort = ReceivePort();
await Isolate.spawn(workerIsolate, mainReceivePort.sendPort);
// 4. The first message is the Worker's SendPort
final completer = Completer<SendPort>();
mainReceivePort.listen((message) {
if (message is SendPort) {
completer.complete(message);
} else {
// 6. Handle subsequent messages (results)
print('[Main] Received: "$message"');
}
});
final workerSendPort = await completer.future;
// 5. Send a task to the Worker
print('[Main] Sending task to Worker...');
workerSendPort.send('task:hello world');
}
Common Pitfall: Forgetting the SendPort Exchange#
When first implementing Isolate communication, a common mistake is forgetting that both sides need the other’s SendPort.
It’s like making a phone call — you called them, but they don’t have your number, so how can they call you back? That’s why the Worker Isolate’s first task is to send its “phone number” (SendPort) to the Main Isolate.
Efficient Data Transfer: TransferableTypedData#
There’s an important performance consideration here.
By default, data sent through a SendPort is Deep Copied. This is fine for small data, but what if you need to send 10MB of image data? A single copy takes a noticeable amount of time.
TransferableTypedData solves this problem. It allows Ownership Transfer instead of copying:
import 'dart:isolate';
import 'dart:typed_data';
void workerIsolate(SendPort sendPort) {
final receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
receivePort.listen((message) {
if (message is TransferableTypedData) {
// materialize() = unwrap the package to get data, can only unwrap once!
final receivedData = message.materialize();
final byteData = receivedData.asUint8List();
print('[Worker] Received data length: ${byteData.length}');
}
});
}
void main() async {
final mainReceivePort = ReceivePort();
await Isolate.spawn(workerIsolate, mainReceivePort.sendPort);
final workerSendPort = await mainReceivePort.first as SendPort;
// Create 10MB of data
final data = Uint8List.fromList(
List.generate(10 * 1024 * 1024, (i) => i % 256)
);
print('[Main] Created data length: ${data.length}');
// Wrap as transferable data
final transferableData = TransferableTypedData.fromList([data]);
// Send - ownership transfers to the Worker
workerSendPort.send(transferableData);
print('[Main] Data sent');
// ⚠️ Note: the original Uint8List is still accessible
// because fromList() copies data into an internal buffer at creation time
print('[Main] Original data still accessible: ${data.length}');
}
Two key points to note:
TransferableTypedData.fromList()copies data into an internal buffer at creation time — the originalUint8Listis unaffected- The receiver unwraps the data via
materialize()— and this can only be done once. Once unwrapped, it’s gone, ensuring no two Isolates access the same memory simultaneously
4. Design Origins: Why Not Erlang?#
This section is the most enlightening part of the talk — Slava Egorov reveals the true story behind Isolate’s design.
Marketing Narrative vs Reality#
- Official Narrative — Dart Isolates inspired by Erlang Actor Model
- Actual Situation — Influenced by JavaScript Web Workers constraints
This isn’t a “scandal”; Slava Egorov was very candid about the design considerations at the time.
The Real Design Context#
Dart’s early architects had worked on Chrome’s V8 engine, and the primary goal at the time was: make Dart compilable to JavaScript to run in browsers.
Design Constrained by Reality#
This reflects an important truth in software design: the best design isn’t necessarily the most ideal design, but the most pragmatic design given the constraints.
The reality facing the Dart team was: JavaScript’s Web Workers work exactly this way — completely isolated, message passing. If Dart’s concurrency model differed too much from JavaScript, compiling to JS would be painful.
So rather than saying Isolates were “inspired by Erlang,” it’s more accurate to say they were “the best solution within JavaScript’s constraints.” Understanding this background helps explain why certain design decisions were made.
Comparison with JavaScript Web Workers#
Dart Isolate:
- Memory Model — Fully isolated, no shared memory
- Communication —
SendPort/ReceivePort - Data Safety — High — no Race Condition risk by design
- Platform — Dart VM core feature
JavaScript Web Worker:
- Memory Model — Default message passing, optional
SharedArrayBuffer - Communication —
postMessage()/onmessage - Data Safety — Manual synchronization needed with
SharedArrayBuffer - Platform — Browser standard feature
Fun Fact: When compiled to Dart Web, the Isolate API is actually built on top of Web Workers. So when you use Isolates in Flutter Web, the underlying implementation is Web Workers.
5. Performance Revolution: Isolate Groups#
This is the major improvement introduced in Dart 2.15, and the most impactful in practice.
Early Isolate Pain Points#
Before Isolate Groups, the experience of using Isolates wasn’t great.
Problem 1: Each Isolate had its own independent Heap and program copy
- Slow startup (sometimes several hundred milliseconds)
- High memory overhead (each Isolate had to load the entire program)
Problem 2: Data transfer required Serialization / Deserialization
- Deep Copy of large data (like JSON) was very time-consuming
- Sometimes the copying took longer than the actual computation
A typical example: delegating JSON parsing to an Isolate, but the total time actually increased. The reason was that passing the JSON string to the Isolate and passing the parsed result back — these two copies cost more than simply parsing directly in the Main Isolate.
This led many early developers to question: are Isolates actually useful?
Isolate Groups: Game Changer#
Isolate Groups introduced in Dart 2.15 completely changed this situation.
The core idea is: let Isolates within the same Group share underlying resources, while still maintaining logical isolation.
How Dramatic Was the Performance Improvement?#
- Isolate startup speed — ~100x faster
- Memory overhead — ~10x less
- Message passing speed — ~8x faster
This isn’t a marginal improvement — it’s an Order of Magnitude Improvement.
Since Isolate Groups launched, the barrier for developers to use Isolates has dropped dramatically. Previously you had to hesitate “is this task worth spinning up an Isolate for?” — now you barely need to think about it since the overhead is negligible.
Directly Transferable Types#
Another important improvement: certain types of objects can be Directly Transferred without any Copy.
- String — immutable, can be shared directly
- Compile-time Constants (
const) — determined at compile time, no copy needed - Deeply Immutable Objects — all fields are final and immutable
- TransferableTypedData — avoids copying through ownership transfer
This is particularly helpful for passing JSON strings — strings are immutable, so they can be shared directly.
Isolate.spawn vs Isolate.spawnUri#
// ✅ Recommended: Spawn within the same Group (fast)
await Isolate.spawn(entryPoint, message);
// ⚠️ Slower: Creates a new Group (use only when needed)
await Isolate.spawnUri(Uri.parse('worker.dart'), [], message);
When would you need spawnUri? When you need to load different Dart code. But in most cases, spawn is sufficient and much faster.
6. Event Loop Model#
Every Isolate has its own Event Loop. If you have a JavaScript background, this concept should be familiar — but some details are worth noting.
Two Queues with Priority Levels#
Why Does This Order Matter?#
A common bug is that UI updates always seem “one step behind.” The cause is usually that the UI update is placed in a Future(), while data processing is in scheduleMicrotask().
Because Microtasks have higher priority, data processing completes first, but the UI update has to wait until the next round of Event processing.
Execution Priority Example#
This example can help build your intuition:
import 'dart:async';
void main() {
print('1. Synchronous code starts');
Future(() => print('4. Event Queue - Future'));
scheduleMicrotask(() => print('3. Microtask Queue'));
Future.microtask(() => print('3.5 Microtask Queue (via Future)'));
Timer(Duration.zero, () => print('5. Event Queue - Timer'));
print('2. Synchronous code ends');
}
// Output order:
// 1. Synchronous code starts
// 2. Synchronous code ends
// 3. Microtask Queue
// 3.5 Microtask Queue (via Future)
// 4. Event Queue - Future
// 5. Event Queue - Timer
Key Observations:
- Synchronous code always executes first
- All Microtasks execute before any Events
Future()andTimer(Duration.zero)are both Events, but Future was registered first so it executes first
7. Practical Application Patterns#
After the theory, let’s look at how to choose in actual development.
Pattern 1: Using Isolate.run() (Recommended since Dart 2.19+)#
This is the simplest approach, suitable for short, one-off tasks:
import 'dart:isolate';
// Top-level function or static method
int expensiveComputation(int input) {
var result = 0;
for (var i = 0; i < input * 1000000; i++) {
result += i;
}
return result;
}
void main() async {
print('Starting computation...');
// Automatically handles Isolate creation, execution, and cleanup
final result = await Isolate.run(() => expensiveComputation(100));
print('Result: $result');
}
When to use this?
- JSON parsing
- Image processing
- Data transformation
- Any “input in, output out” pure function computation
Practical advice: 90% of Isolate scenarios can use this. The API is clean, no need to handle low-level details like SendPort/ReceivePort.
Pattern 2: Using Flutter compute()#
A convenience function provided by Flutter, essentially a wrapper around Isolate.run():
import 'package:flutter/foundation.dart';
import 'dart:convert';
// Must be a top-level function or static method
List<String> _parseAndExtractNames(String jsonString) {
final List<dynamic> users = jsonDecode(jsonString);
return users.map((user) => user['name'] as String).toList();
}
Future<void> processUsers() async {
String largeJsonArray = '''
[{"name": "Alice"}, {"name": "Bob"}, {"name": "Charlie"}]
''';
print("Parsing JSON in background Isolate...");
// UI Thread won't be blocked
List<String> names = await compute(_parseAndExtractNames, largeJsonArray);
print("Parsing complete: $names");
}
Recommendation: Prefer
Isolate.run()for new projects.compute()was Flutter’s early convenience API for simplifying Isolate usage. SinceIsolate.run()was introduced (Dart 2.19), official documentation has gradually recommended usingIsolate.run()directly to reduce unnecessary dependency on the Flutter framework.
Pattern 3: Long-Running Worker Isolate#
Suitable for scenarios requiring maintained state or continuous communication:
import 'dart:async';
import 'dart:isolate';
class WorkerManager {
Isolate? _isolate;
SendPort? _workerSendPort;
ReceivePort? _mainReceivePort;
StreamSubscription? _subscription;
final _responseController = StreamController<dynamic>.broadcast();
Stream<dynamic> get responses => _responseController.stream;
Future<void> initialize() async {
_mainReceivePort = ReceivePort();
// Start the Worker Isolate
_isolate = await Isolate.spawn(
_workerEntry,
_mainReceivePort!.sendPort,
);
// Listen for messages from the Worker
final completer = Completer<SendPort>();
_subscription = _mainReceivePort!.listen((message) {
if (message is SendPort) {
// First message is the Worker's SendPort (handshake complete)
completer.complete(message);
} else {
// Forward subsequent messages to the response stream
_responseController.add(message);
}
});
_workerSendPort = await completer.future;
}
static void _workerEntry(SendPort mainSendPort) {
final workerReceivePort = ReceivePort();
// Handshake: send own SendPort back to Main Isolate
mainSendPort.send(workerReceivePort.sendPort);
// Worker internal state
var counter = 0;
workerReceivePort.listen((message) {
if (message == 'increment') {
counter++;
mainSendPort.send('Counter: $counter');
} else if (message == 'reset') {
counter = 0;
mainSendPort.send('Counter reset');
}
});
}
void sendTask(String task) {
_workerSendPort?.send(task);
}
void dispose() {
_subscription?.cancel();
_mainReceivePort?.close();
_responseController.close();
_isolate?.kill(priority: Isolate.immediate);
}
}
// Usage example
void main() async {
final worker = WorkerManager();
await worker.initialize();
worker.responses.listen((message) {
print('Received response: $message');
});
worker.sendTask('increment');
worker.sendTask('increment');
worker.sendTask('reset');
await Future.delayed(Duration(seconds: 1));
worker.dispose();
}
When do you need a long-running Worker?
- Background services with continuous monitoring
- Need to maintain state within the Isolate (e.g., cache)
- WebSocket or real-time messaging
- High-frequency tasks: avoid the overhead of repeatedly creating/destroying Isolates
How to Choose?#
- One-time computation (JSON parsing, encryption, etc.) —
Isolate.run()orcompute() - Pure Dart projects —
Isolate.run() - Flutter background computation —
compute()orIsolate.run()(either works) - Need continuous communication or maintained state —
Isolate.spawn()+ Worker pattern - High-frequency repeated tasks — Custom Worker or Isolate Pool package
- Large binary data transfer — Use with
TransferableTypedData
8. Interacting with Native Platforms#
This section is more advanced, but if you’ve worked with Platform Channels or FFI, you’ll find it quite enlightening.
Flutter UI Thread Evolution#
Traditionally, the Dart UI Isolate and the Native UI Thread were separate, communicating asynchronously via Platform Channels:
What does this mean? Dart can make synchronous calls to platform APIs, without going through the asynchronous conversion of Platform Channels.
FFI and Isolate Integration#
If you use FFI to call C libraries, there’s one important consideration: dart:ffi has no async API — all FFI calls are synchronous. This means heavy FFI calls on the Main Isolate will freeze the UI. The solution is to move FFI operations to a Worker Isolate, and the modern approach is to use Isolate.run() directly:
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
// FFI type definitions
typedef HeavyComputationNative = Int64 Function(Int64);
typedef HeavyComputationDart = int Function(int);
int _runHeavyComputation(int input) {
// Load native library
final path = Platform.isAndroid ? 'libheavy.so' : 'libheavy.dylib';
final dylib = DynamicLibrary.open(path);
// Lookup function
final heavyComputation = dylib
.lookup<NativeFunction<HeavyComputationNative>>('heavy_computation')
.asFunction<HeavyComputationDart>();
return heavyComputation(input); // Blocking call, but UI unaffected since we're in Worker Isolate
}
Future<int> runHeavyComputationInBackground(int input) async {
// Isolate.run() handles Isolate spawning, execution, and cleanup automatically
return await Isolate.run(() => _runHeavyComputation(input));
}
Compared to manual Isolate.spawn() + ReceivePort, this is half the length — all the common boilerplate handed off to the Dart SDK.
NativeCallable: Modern Callback Pattern#
FFI has another common need: native code calling Dart functions (e.g., C library triggering events, notifying you after async operations complete). Previously this only worked with Pointer.fromFunction, which had two limitations: must be static functions (can’t capture variables), and can only be called from the thread that created it. Dart 3.1 / 3.2 introduced NativeCallable to solve both pain points at once:
NativeCallable.isolateLocal(Dart 3.2+, stable) — Closure-capable upgrade fromPointer.fromFunctionNativeCallable.listener(Dart 3.1+, stable) — Any native thread can invoke it, callback is posted to the originating Isolate’s event queue, native side doesn’t blockNativeCallable.isolateGroupBound(~3.9, experimental) — Synchronous cross-thread call, tied to the Shared Memory Multithreading proposal
For most “C async event notifying Dart” scenarios, NativeCallable.listener is the standard answer today — solving both thread safety and avoiding blocking the native thread.
Reverse Call Challenge#
Back to the problem from the talk: what if native code wants to synchronously call Dart?
The async direction is now solved — NativeCallable.listener is the answer.
What remains unsolved is the synchronous reverse call — where native side waits for Dart to finish before continuing. This runs into:
- Option A: Use existing Isolate — may be busy or blocked on await
- Option B: Spawn temporary Isolate — can’t access existing state, startup overhead
The current experimental NativeCallable.isolateGroupBound (tied to the Shared Memory Multithreading proposal) is the direction being explored, but not yet stable. This remains one of the trickier, continuously evolving parts of Dart/Flutter architecture.
9. Web Platform Challenges#
If your Flutter project needs to support Web, pay special attention to this section.
Historical Limitations and Current State#
Dart Web in its early days did not support the full Isolate API. This was because browser Web Workers have inherent limitations:
- Cannot share memory (except
SharedArrayBuffer) SharedArrayBufferis restricted for security reasons (Spectre/Meltdown)- Cannot optimize Isolate Groups like the Dart VM
This is the flip side of Dart’s original design being “constrained by Web Workers” — to compile to Web, you must accept Web’s limitations.
However, since Dart 3.7+, the situation has improved significantly. Isolate.run() now works on the Web platform (automatically mapped to Web Workers under the hood). For one-off “input in, output out” tasks, the cross-platform code is now consistent.
Limitations that still apply:
- Isolate Groups performance advantages do not apply on Web — each Web Worker is still an independent JS environment
- Long-running Worker patterns may have subtle behavioral differences
- Isolate startup overhead is higher on Web — Web Worker initialization is much slower than Dart VM’s Isolate spawn
Practical Recommendations#
For projects that need to support both Web and Native:
import 'dart:isolate';
Future<Result> processData(Data data) async {
// Since Dart 3.7+, Isolate.run() works on both Web and Native
return Isolate.run(() => _processDataSync(data));
}
If you need to support older Dart versions, or encounter Web Worker performance bottlenecks, consider Conditional Compilation:
import 'package:flutter/foundation.dart' show kIsWeb;
Future<Result> processData(Data data) async {
if (kIsWeb) {
// Web: Process on main thread (avoid Worker startup overhead)
return _processDataSync(data);
} else {
// Native: Use Isolate
return Isolate.run(() => _processDataSync(data));
}
}
Future Outlook#
The Dart team is actively pursuing Wasm compilation. Once WebAssembly’s shared memory proposal matures and Dart-to-Wasm compilation stabilizes, Isolate support on the Web platform should improve further, especially in terms of performance and Isolate Groups optimization.
10. Future Direction: Shared Memory#
Slava Egorov’s Candid Admission#
“Although Shared Memory Multithreading is scary, it’s unavoidable for performance.”
This statement is interesting. The Dart team spent years building a “Fully Isolated” model — why open up Shared Memory now?
The answer is: in certain scenarios, the cost of Copy is simply too high.
Consider game development — a physics engine needs to update the positions of thousands of objects every frame. If you had to copy this data between Isolates every time, performance simply couldn’t keep up.
Shared Memory Multithreading Proposal#
The Dart team is designing a selective, safe memory sharing mechanism, progressing in two phases:
- Phase 1: Shared Native Memory (current focus) — supports sharing native memory types (typed data, FFI pointers). Core implementation merged into SDK but still behind experimental flag, not yet in stable
- Phase 2: Shared Anything (future) — sharing general Dart objects, not started yet
The following is the API direction in the current proposal (still subject to change):
// 1. Shared static fields (using the shared keyword)
shared static int sharedCounter = 0;
// 2. Shared Isolate -- can only access shared state
void sharedWorker() {
// Can only access data marked as shared
sharedCounter++;
}
// 3. dart:concurrent library
import 'dart:concurrent';
final mutex = Mutex();
final atomicInt = AtomicInt64(0);
void safeIncrement() {
mutex.lock();
try {
atomicInt.value++;
} finally {
mutex.unlock();
}
}
// 4. New Isolate API
await Isolate.runShared(() {
// Execute in a shared Isolate
sharedCounter++;
});
Design Principles#
Design Philosophy#
This design direction is quite clever — rather than completely overthrowing the existing model, it provides an Escape Hatch when needed.
Most developers can continue to enjoy the safety of “not worrying about Race Conditions,” while scenarios requiring extreme performance (games, audio processing, AI inference, etc.) can opt into Shared Memory.
This “safe by default, opt-in for power” design philosophy is becoming increasingly common in modern language design (Rust’s unsafe, Kotlin’s !! operator, etc.).
Expected Use Cases#
- High-performance Native Interop: Share Memory with C++ libraries, avoiding Copy Overhead
- Shared Cache: Multiple Isolates reading the same Read-only Data
- High-frequency Updates: Game Physics Engines, Audio Processing, and other Low Latency scenarios
Summary#
Looking back at this talk, what struck me most was the Dart team’s pragmatic attitude toward concurrency — not chasing the perfect Actor Model, but finding balance between “JavaScript compilation constraints,” “developer safety,” and “performance needs.”
The community’s view of Isolate has also undergone a clear transition:
- Before Dart 2.15: Many developers complained that “opening an Isolate is slower than not opening one” — data copy costs were simply too high. This period saw several community packages (worker_manager, computer, isolate_agents) trying to encapsulate the complexity
- After Dart 2.15: Isolate Groups brought 100x startup speed improvement, dramatically changing community perception. The native API became usable, reducing the need for third-party packages
- Current debate: The Shared Memory proposal has sparked discussion — some worry it’ll break Dart’s “safe by default” brand, others argue that selective opening is a necessary compromise for scenarios like games and audio processing
The most valuable thing about this talk is that it shows a concurrency model isn’t “designed” but “evolved” — from the Web compilation compromise, to the 2.15 performance revolution, to the future Shared Memory proposal, each step is a response to real needs.
The Evolution of Isolates#
Key Takeaways#
- An Isolate is a Space, not a Thread — This Mental Model shift is the foundation for understanding Dart Concurrency
- Isolate Groups are a Game Changer — You can now confidently use Isolates without worrying about Performance Overhead
- Choosing the right pattern matters:
- Simple tasks →
Isolate.run()orcompute() - Long-running tasks →
Isolate.spawn()+ continuous communication - Large data →
TransferableTypedData
4. Be careful with the Web platform — Isolate support on Web is limited; prepare alternatives
5. The future will be more flexible — Shared Memory is on the way, but the core Isolation Model will remain
Getting Started Advice#
When first diving into Dart concurrency, we recommend:
- Start with
Isolate.run()orcompute()— don’t jump straight into complex Worker patterns - Measure before optimizing — don’t assume “this task needs an Isolate”; sometimes running directly is actually faster
- Understand the Event Loop — this is very helpful for debugging async issues
- Plan ahead for Web — if your project needs Web support, invest early in Conditional Compilation
References and Resources#
Talk: What is an Isolate anyway?
Speaker: Slava Egorov — Technical Lead, Dart Team
- Dart Official: Concurrency in Dart
- Flutter Official: Isolates and Event Loops
- Dart: C interop (FFI)
- Dart API: NativeCallable
- Dart Isolate Groups (Completed)
- Shared Memory Multithreading Proposal
- Shared Memory Multithreading Design Doc
Other Articles#
本文原刊登於 Medium。