— Flutter, Dart, Performance, Concurrency — 3 min read
When building Flutter applications, maintaining a smooth and responsive user interface is crucial. However, performing heavy computations on the main thread can cause UI jank and make your app feel sluggish. This is where Isolates come in - Dart's way of achieving true parallel execution.
In Dart, an isolate is an independent worker that runs code in parallel with the main program. Unlike threads in other languages, isolates don't share memory - instead, they communicate by passing messages. This design eliminates many common concurrency issues like race conditions and deadlocks.
Think of isolates as separate processes within your app. Each isolate has:
Flutter apps run on a single thread by default, handling both UI rendering and your application logic. When you perform a heavy operation like:
...the UI can freeze while waiting for these operations to complete. Isolates solve this problem by moving heavy work off the main thread.
Here's a simple example of using an isolate to perform a heavy computation:
1import 'dart:isolate';2
3// Function that will run in the isolate4void heavyComputation(SendPort sendPort) {5 // Perform heavy work here6 int result = 0;7 for (int i = 0; i < 1000000000; i++) {8 result += i;9 }10 11 // Send the result back to the main isolate12 sendPort.send(result);13}14
15// In your main code16Future<void> runHeavyTask() async {17 // Create a receive port to get messages from the isolate18 final receivePort = ReceivePort();19 20 // Spawn the isolate21 await Isolate.spawn(heavyComputation, receivePort.sendPort);22 23 // Listen for the result24 final result = await receivePort.first;25 print('Result: $result');26 27 // Close the port when done28 receivePort.close();29}One of the most common use cases for isolates is parsing large JSON responses:
1import 'dart:convert';2import 'dart:isolate';3
4// The function that will parse JSON in the isolate5void parseJsonInIsolate(List<dynamic> args) {6 SendPort sendPort = args[0];7 String jsonString = args[1];8 9 // Parse the JSON10 final parsed = jsonDecode(jsonString);11 12 // Send back the result13 sendPort.send(parsed);14}15
16// Function to call from your main code17Future<dynamic> parseJsonAsync(String jsonString) async {18 final receivePort = ReceivePort();19 20 // Pass both the SendPort and the JSON string21 await Isolate.spawn(22 parseJsonInIsolate,23 [receivePort.sendPort, jsonString],24 );25 26 final result = await receivePort.first;27 receivePort.close();28 29 return result;30}31
32// Usage33Future<void> loadData() async {34 String largeJsonString = '{"data": [...]}'; // Your large JSON35 final parsed = await parseJsonAsync(largeJsonString);36 // Use the parsed data37}Flutter provides a convenient helper function called compute() that simplifies working with isolates for one-off computations:
1import 'package:flutter/foundation.dart';2
3// Your computation function (must be top-level or static)4int fibonacci(int n) {5 if (n <= 1) return n;6 return fibonacci(n - 1) + fibonacci(n - 2);7}8
9// Using compute10Future<void> calculateFibonacci() async {11 final result = await compute(fibonacci, 45);12 print('Fibonacci result: $result');13}The compute() function handles all the isolate management for you - creating the isolate, passing the message, receiving the result, and cleaning up.
Dart 2.19 introduced Isolate.run(), which provides an even simpler way to run code in an isolate. Unlike the traditional approach with Isolate.spawn(), Isolate.run() can work with closures (anonymous functions), making your code more concise:
1import 'dart:isolate';2
3// Traditional approach - requires top-level or static function4Future<int> traditionalApproach(int value) async {5 final receivePort = ReceivePort();6 await Isolate.spawn(_compute, [receivePort.sendPort, value]);7 return await receivePort.first;8}9
10static void _compute(List<dynamic> args) {11 SendPort sendPort = args[0];12 int value = args[1];13 sendPort.send(value * 2);14}15
16// Modern approach with Isolate.run() - can use closures!17Future<int> modernApproach(int value) async {18 return await Isolate.run(() {19 // This closure runs in a separate isolate20 return value * 2;21 });22}The beauty of Isolate.run() is its simplicity. Here's a more practical example:
1import 'dart:isolate';2import 'dart:convert';3
4Future<List<User>> parseUsers(String jsonString) async {5 return await Isolate.run(() {6 final List<dynamic> jsonList = jsonDecode(jsonString);7 return jsonList.map((json) => User.fromJson(json)).toList();8 });9}10
11// Usage12final users = await parseUsers(response.body);When to use Isolate.run() vs compute():
Isolate.run() when you have access to pure Dart (dart:isolate)compute() in Flutter widgets where you're importing package:flutter/foundation.dartFor more complex scenarios where you need ongoing communication with an isolate, you can set up bidirectional messaging:
1class IsolateWorker {2 late Isolate _isolate;3 late ReceivePort _receivePort;4 late SendPort _sendPort;5 6 Future<void> init() async {7 _receivePort = ReceivePort();8 _isolate = await Isolate.spawn(_isolateEntry, _receivePort.sendPort);9 _sendPort = await _receivePort.first;10 }11 12 static void _isolateEntry(SendPort sendPort) {13 final receivePort = ReceivePort();14 sendPort.send(receivePort.sendPort);15 16 receivePort.listen((message) {17 // Process message and send response18 final result = message * 2; // Example computation19 sendPort.send(result);20 });21 }22 23 Future<dynamic> sendMessage(dynamic message) async {24 _sendPort.send(message);25 return await _receivePort.first;26 }27 28 void dispose() {29 _isolate.kill(priority: Isolate.immediate);30 _receivePort.close();31 }32}33
34// Usage35final worker = IsolateWorker();36await worker.init();37final result = await worker.sendMessage(42);38print('Result: $result'); // Prints: Result: 8439worker.dispose();Not all data types can be sent between isolates. You can send:
@immutableYou cannot send:
Creating an isolate has overhead - it's not free. For very quick operations, the cost of spawning an isolate might exceed the benefit. Use isolates for genuinely heavy operations that take more than a few milliseconds.
When testing code that uses isolates, be aware that isolates run asynchronously. Make sure your tests properly await isolate completion:
1test('isolate computation', () async {2 final result = await compute(fibonacci, 10);3 expect(result, equals(55));4});Use Isolate.run() or compute() for Simple Cases: For one-off computations, these helper functions are cleaner and easier to use than manually managing isolates. Use Isolate.run() for pure Dart projects and compute() in Flutter widgets.
Keep Functions Pure: Functions running in isolates should be pure functions - they take input, process it, and return output without side effects.
Make Functions Top-Level or Static (When Required): When using Isolate.spawn() or compute(), the entry point function must be a top-level function or a static method. One of the main advantages of the modern Isolate.run() helper is that it bypasses this limitation and can execute a closure directly.
Clean Up Resources: Always properly dispose of isolates and close receive ports when you're done with them to avoid memory leaks.
Consider the Trade-offs: Not every operation needs an isolate. Profile your app to identify true bottlenecks before adding the complexity of isolates.
Here are some scenarios where isolates shine:
Isolates are a powerful tool in the Flutter developer's toolkit for maintaining smooth UI performance while handling heavy computations. By understanding when and how to use them effectively, you can build apps that feel responsive and performant even when performing complex operations.
Remember: the key is balance. Not every operation needs an isolate, but when you have genuinely heavy work that's causing UI jank, isolates provide a clean, safe way to parallelize that work without the typical headaches of concurrent programming.
Start with modern helpers like Isolate.run() or compute() for simple cases, and graduate to manual isolate management when you need more control over the lifecycle and communication patterns. Your users will thank you for the smooth, responsive experience!