← Articles

Dart isolates: true parallelism in Flutter

By Ann Tech · 19 May 2026

Dart is single-threaded within an isolate — but you can spawn additional isolates that run in parallel on separate threads. Unlike threads in Java/Kotlin, isolates don't share memory. They communicate by passing messages, which eliminates entire classes of concurrency bugs.

When to use isolates

Use an isolate when a task takes more than ~2ms on the UI thread and causes dropped frames:

  • Parsing large JSON responses (>100 KB)
  • Image processing or compression
  • Encryption/decryption of large data
  • Complex calculations (route planning, ML inference)
  • File processing (CSV parsing, PDF generation)

Don't use isolates for network calls or database queries — those are already async and don't block the thread.

compute() — the simplest option

For a one-shot computation with a single input and output:

// Define the function at the top level (not a closure or instance method)
List<Product> _parseProducts(String json) {
  final list = jsonDecode(json) as List;
  return list
      .map((e) => Product.fromJson(e as Map<String, dynamic>))
      .toList();
}

// In your repository/service:
Future<List<Product>> fetchProducts() async {
  final response = await dio.get('/products');
  // Offload parsing to background isolate
  return compute(_parseProducts, response.data as String);
}

compute() spawns an isolate, runs the function, returns the result, and disposes the isolate. Zero lifecycle management.

Isolate.run() — Dart 2.19+

Cleaner syntax for the same pattern, supports closures:

Future<List<Product>> fetchProducts() async {
  final response = await dio.get('/products');
  final raw = response.data as String;

  return Isolate.run(() {
    final list = jsonDecode(raw) as List;
    return list
        .map((e) => Product.fromJson(e as Map<String, dynamic>))
        .toList();
  });
}

Long-running isolate with SendPort/ReceivePort

For an isolate that processes multiple messages over its lifetime (e.g., a background sync worker):

class BackgroundWorker {
  late final Isolate _isolate;
  late final SendPort _sendPort;
  final _receivePort = ReceivePort();

  Future<void> spawn() async {
    _isolate = await Isolate.spawn(_workerMain, _receivePort.sendPort);
    _sendPort = await _receivePort.first as SendPort;
  }

  Future<String> process(String data) async {
    final response = ReceivePort();
    _sendPort.send([data, response.sendPort]);
    return await response.first as String;
  }

  void dispose() {
    _isolate.kill();
    _receivePort.close();
  }
}

// Top-level function — runs in the isolate
void _workerMain(SendPort mainSendPort) {
  final port = ReceivePort();
  mainSendPort.send(port.sendPort);

  port.listen((message) {
    final list = message as List;
    final data = list[0] as String;
    final replyPort = list[1] as SendPort;

    // Do expensive work here
    final result = _processData(data);
    replyPort.send(result);
  });
}

What can't be sent between isolates

Isolates only pass messages, not references. Only these types can be sent:

  • Primitive types (int, double, bool, String)
  • Lists and Maps of sendable types
  • SendPort
  • TransferableTypedData (zero-copy for binary data)

Classes with native resources (sockets, file handles, database connections) cannot be sent. You can send data, not objects that hold platform resources.

FlutterIsolate for packages that need Flutter bindings

Standard Isolate.spawn can't use Flutter plugins. Use flutter_isolate package if your isolate needs to call platform channels:

import 'package:flutter_isolate/flutter_isolate.dart';

// @pragma annotation required
@pragma('vm:entry-point')
void isolateMain(SendPort sendPort) async {
  // Can use Flutter plugins here
  final prefs = await SharedPreferences.getInstance();
  sendPort.send(prefs.getString('key'));
}

final isolate = await FlutterIsolate.spawn(isolateMain, receivePort.sendPort);

Common pitfalls

Sending class instances with methods. Only the data fields are copied — methods and closures don't transfer. Send plain data (Maps, Lists, primitives) and reconstruct objects on the receiving end.

Spawning a new isolate for every small task. Spawning an isolate has overhead (~10ms). For many small tasks, use a persistent isolate worker and send tasks to it. Use compute() only for tasks that take > 50ms.

Forgetting to kill long-running isolates. An isolate spawned with Isolate.spawn runs until it's killed or exits itself. Always call isolate.kill() in dispose() to avoid leaks.

Sign in to like, dislike, or report.

Dart isolates: true parallelism in Flutter — ANN Tech