Using Isolates and the compute Function in Flutter to Offload Heavy Computation
This article explains how to prevent UI blocking in Flutter by moving intensive calculations to background isolates using the compute helper and raw Isolate APIs, demonstrates code examples for task execution, progress reporting, and inter‑isolate communication, and discusses performance considerations and best practices.
The article introduces a performance problem: calculating the average of 100 million random numbers (1‑10000) on the main UI thread blocks the Flutter interface, causing the FloatingActionButton ripple and other UI elements to freeze.
It first shows a naive implementation where the _doTask method runs the loop directly, taking about 8.5 seconds and blocking the UI.
To avoid blocking, two approaches are discussed: delegating the heavy work to a backend service (IO task) or using multithreading within Dart. The focus shifts to the latter, specifically the Isolate mechanism.
The compute function is presented as a convenient wrapper around isolates. Its signature, generic parameters, and usage are explained. A TaskResult class is defined to hold the computation time and result, and an example shows how to call compute with a static async callback _doTaskInCompute .
void _doTask() async {
TaskResult taskResult = await compute
(
_doTaskInCompute,
'',
debugLabel: "task1");
setState(() {
result = taskResult.result;
cost = taskResult.cost;
});
}
static Random random = Random();
static Future
_doTaskInCompute(String arg) async {
int count = 100000000;
double result = 0;
int cost = 0;
int sum = 0;
int startTime = DateTime.now().millisecondsSinceEpoch;
for (int i = 0; i < count; i++) {
sum += random.nextInt(10000);
}
int endTime = DateTime.now().millisecondsSinceEpoch;
result = sum / count;
cost = endTime - startTime;
return TaskResult(result: result, cost: cost);
}Using compute keeps the UI responsive; the indicator continues animating while the background isolate performs the calculation.
The article then dives into the internal implementation of compute , showing how it creates a RawReceivePort , a Completer , and spawns an isolate via Isolate.spawn . It explains the message‑passing flow: the isolate sends a single‑element list as the result, which the completer completes.
Limitations of compute are highlighted: it only returns the final result, offering no progress updates. To provide progress, the article demonstrates using Isolate.spawn directly with a RawReceivePort and SendPort for two‑way communication.
void _doTask() async {
final receivePort = RawReceivePort();
receivePort.handler = handleMessage;
await Isolate.spawn(
_doTaskInCompute,
receivePort.sendPort,
onError: receivePort.sendPort,
onExit: receivePort.sendPort,
);
}
static void _doTaskInCompute(SendPort port) async {
int count = 100000000;
double result = 0;
int sum = 0;
int startTime = DateTime.now().millisecondsSinceEpoch;
for (int i = 0; i < count; i++) {
sum += random.nextInt(10000);
if (i % 1000000 == 0) {
// progress update
port.send(i / count);
}
}
int endTime = DateTime.now().millisecondsSinceEpoch;
result = sum / count;
int cost = endTime - startTime;
Isolate.exit(port, TaskResult(result: result, cost: cost));
}
void handleMessage(dynamic msg) {
print("========= $msg ===========");
if (msg is TaskResult) {
progress = 1;
setState(() {
result = msg.result;
cost = msg.cost;
});
}
if (msg is double) {
setState(() {
progress = msg;
});
}
}Progress messages (as double values) are received and used to update a UI progress bar, while the final TaskResult marks completion.
Finally, the article advises cautious use of isolates: they consume memory (~30 KB) and have overhead, so they should be reserved for genuinely heavy or parallelizable tasks such as large text parsing or image processing, not for trivial micro‑second operations.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.