Flutter Isolates: Complete Guide to Background Tasks

Flutter Isolates
Flutter Isolates

When building Flutter applications, performance is one of the most important things to consider. Users expect applications to open quickly, respond smoothly, and handle data without freezing the screen.

However, sometimes an application needs to perform heavy operations such as parsing large JSON responses, processing images, filtering thousands of records, or performing complex calculations. If these operations run on the main isolate, they can make the user interface slow or unresponsive.

This is where Flutter Isolates become useful.

Dart provides isolates to execute CPU-intensive tasks independently from the main isolate. They help developers perform expensive computations without blocking the user interface.

In this tutorial, you will learn what isolates are, how they work, how to use Isolate.run(), how to use Flutter’s compute() function, and how to communicate between multiple isolates using SendPort and ReceivePort.

1. What Are Isolates in Flutter?

An isolate is an independent execution environment in Dart. Every isolate has its own memory and event loop.

Unlike traditional threads that can share memory, Dart isolates do not share mutable state. They communicate with each other by sending messages.

By default, a Flutter application runs its UI-related work on the main isolate. This isolate handles tasks such as:

  • Processing user interactions.
  • Building and updating widgets.
  • Handling application events.
  • Rendering frames.
  • Running normal application logic.

When a heavy computation runs on the main isolate, it can prevent the application from processing UI events on time.

A worker isolate allows you to move that computation away from the main isolate.

Simple example

Imagine your application downloads a large JSON file containing 50,000 user records.

The application needs to decode the JSON and convert it into Dart objects.

If the decoding operation takes too long on the main isolate, the screen may freeze temporarily.

Instead, you can move the JSON parsing operation to another isolate. The main isolate remains available to handle UI updates while the worker isolate processes the data.

2. Why Do We Need Isolates in Flutter?

Flutter applications frequently perform operations that require significant CPU processing.

Some examples include:

  • Parsing large JSON responses.
  • Processing or compressing images.
  • Performing complex mathematical calculations.
  • Filtering large collections of records.
  • Reading and processing large files.
  • Converting audio or video data.
  • Processing large datasets.

Consider the following example:

void processLargeData() {
  int total = 0;

  for (int i = 0; i < 1000000000; i++) {
    total += i;
  }

  print(total);
}

This function performs a large number of calculations.

If you call it directly from a button’s onPressed callback, it can block the main isolate until the calculation finishes.

ElevatedButton(
  onPressed: () {
    processLargeData();
  },
  child: const Text('Calculate'),
)

During that time, the UI may become unresponsive.

Using an isolate allows the calculation to run independently.

Important: You don’t need isolates for every asynchronous operation. Network requests and many file operations already have asynchronous APIs. Isolates are particularly useful when CPU-heavy synchronous work causes UI jank.

3. Main Isolate vs Worker Isolate

Dart applications can use multiple isolates to perform independent tasks.

Let’s understand the difference between the main isolate and a worker isolate.

Main Isolate

The main isolate is responsible for the application’s primary execution and Flutter UI.

Its responsibilities include:

  • Managing widget updates.
  • Handling user interactions.
  • Processing UI events.
  • Updating application state.
  • Coordinating screen rendering.

Worker Isolate

A worker isolate performs computational tasks independently.

Its responsibilities may include:

  • Parsing large JSON data.
  • Performing calculations.
  • Processing files.
  • Filtering large datasets.
  • Preparing data for the UI.

Main Isolate vs Worker Isolate

Main IsolateWorker Isolate
Handles Flutter UIHandles background computation
Processes user interactionsProcesses independent tasks
Builds and updates widgetsCannot directly update widgets
Can become blocked by heavy CPU workKeeps heavy work away from the UI
Uses its own memoryHas separate memory

Remember that a worker isolate cannot directly access the main isolate’s variables or update Flutter widgets. It must return results through message passing.

4. How Do Flutter Isolates Work?

Dart isolates follow a message-passing model.

Each isolate has its own memory, so one isolate cannot directly modify another isolate’s variables.

For communication, Dart provides two important classes:

  • SendPort: Used to send messages.
  • ReceivePort: Used to receive messages.

The basic process looks like this:

  1. The main isolate creates a worker isolate.
  2. The main isolate sends data to the worker.
  3. The worker performs the requested computation.
  4. The worker sends the result back.
  5. The main isolate receives the result and updates the UI.

This approach avoids shared mutable state and makes concurrent programming easier to manage.

5. Create an Isolate Using Isolate.run()

Dart provides Isolate.run() to execute a one-time computation in a separate isolate.

It is one of the simplest ways to perform CPU-intensive work outside the main isolate.

First, import the Dart isolate library:

import 'dart:isolate';

Now create a function that performs a heavy calculation.

int calculateTotal() {
  int total = 0;

  for (int i = 0; i < 100000000; i++) {
    total += i;
  }

  return total;
}

You can execute this function using Isolate.run():

Future<void> performCalculation() async {
  final result = await Isolate.run(() {
    return calculateTotal();
  });

  print('Result: $result');
}

Here is what happens:

  • Isolate.run() starts a new isolate.
  • The calculation runs inside that isolate.
  • The result is returned to the calling isolate.
  • The temporary isolate shuts down after the computation finishes.

Because the calculation runs independently, it does not block the main isolate.

Using Isolate.run() in a Flutter button

You can call the function from a Flutter widget:

ElevatedButton(
  onPressed: () async {
    final result = await Isolate.run(() {
      int total = 0;

      for (int i = 0; i < 100000000; i++) {
        total += i;
      }

      return total;
    });

    print('Calculation Result: $result');
  },
  child: const Text('Calculate'),
)

For production applications, it is better to keep heavy computation in a separate function instead of writing complex logic directly inside the button callback.

Note: Values captured by the callback and the returned result must be transferable between isolates. Avoid capturing objects such as open sockets, controllers, or other resources that cannot be sent between isolates.

6. Using compute() in Flutter

Flutter provides a convenient function called compute() from the flutter/foundation.dart package.

It is useful when you want to execute a function with an input value and receive its result.

Import the required package:

import 'package:flutter/foundation.dart';

The basic syntax is:

final result = await compute(functionName, inputData);

Here:

  • functionName is the function that performs the computation.
  • inputData is the value passed to that function.
  • result contains the returned value.

Let’s understand this with an example.

Step 1: Create a background function

int calculateSquare(int number) {
  return number * number;
}

Step 2: Call compute()

Future<void> calculateResult() async {
  final result = await compute(calculateSquare, 10);

  print(result);
}

Output:

100

The compute() function runs the computation using an isolate on native platforms such as Android and iOS.

On Flutter web, it runs the computation on the current event loop instead of creating a separate isolate.

Important: Keep the callback and its input/output values compatible with isolate message passing. A top-level or static function is a good choice for the callback.

7. Parse Large JSON Data Using Flutter Isolates

One of the most practical use cases of Flutter Isolates is parsing large JSON responses.

Imagine your application receives thousands of user records from an API.

You can fetch the response asynchronously and move the expensive JSON decoding work to another isolate.

For this example, install the HTTP package:

flutter pub add http

Step 1: Import packages

import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;

Step 2: Create a JSON parsing function

Create a top-level function that converts the JSON response into a list of maps.

List<Map<String, dynamic>> parseUsers(String responseBody) {
  final decoded = jsonDecode(responseBody) as List<dynamic>;

  return decoded.map((item) {
    return Map<String, dynamic>.from(item as Map);
  }).toList();
}

This function decodes the JSON and converts each record into a Dart map.

Step 3: Fetch data and use compute()

Future<void> fetchUsers() async {
  final url = Uri.parse(
    'https://jsonplaceholder.typicode.com/users',
  );

  final response = await http.get(url);

  if (response.statusCode == 200) {
    final users = await compute(parseUsers, response.body);

    print(users);
  } else {
    throw Exception('Failed to load users');
  }
}

How does this work?

First, the HTTP request downloads the response.

Next, compute() sends the response body to the parsing function.

The JSON decoding and conversion happen in a separate isolate on native platforms. Finally, the parsed data is returned to the main isolate.

This is especially useful when working with large responses that take noticeable time to decode.

Remember: The network request itself is already asynchronous. The isolate is used for CPU-intensive JSON parsing, not simply for waiting for the API response.

8. Use Isolates for Heavy Calculations

Another common use case is performing complex mathematical calculations.

For example, consider an application that needs to calculate the sum of a large range of numbers.

You can use Isolate.run():

import 'dart:isolate';

Future<int> calculateLargeSum() async {
  return await Isolate.run(() {
    int sum = 0;

    for (int i = 1; i <= 100000000; i++) {
      sum += i;
    }

    return sum;
  });
}

Now call it from your widget:

ElevatedButton(
  onPressed: () async {
    final result = await calculateLargeSum();

    print('Total: $result');
  },
  child: const Text('Calculate Sum'),
)

The calculation runs in a separate isolate, and the result becomes available when the operation finishes.

This approach can be useful for:

  • Large mathematical calculations.
  • Data transformations.
  • Complex filtering operations.
  • Processing large collections.
  • CPU-intensive algorithms.

9. Communicate Between Isolates Using SendPort and ReceivePort

So far, we have used Isolate.run() and compute() for one-time tasks.

But what if you need a worker isolate that stays active and processes multiple messages?

For this purpose, Dart provides Isolate.spawn(), SendPort, and ReceivePort.

Let’s create a simple worker isolate.

Step 1: Import dart

import 'dart:isolate';

Step 2: Create a worker function

The worker function receives a SendPort and sends a result back to the main isolate.

void heavyTask(SendPort sendPort) {
  int total = 0;

  for (int i = 0; i < 100000000; i++) {
    total += i;
  }

  sendPort.send(total);
}

Step 3: Create and start the isolate

Future<void> startWorker() async {
  final receivePort = ReceivePort();

  final isolate = await Isolate.spawn(
    heavyTask,
    receivePort.sendPort,
  );

  final result = await receivePort.first;

  print('Result: $result');

  receivePort.close();

  isolate.kill(priority: Isolate.immediate);
}

Understanding the code

Let’s understand the important parts:

ReceivePort: Creates a communication channel for receiving messages.

SendPort: Allows the worker isolate to send a message back to the main isolate.

Isolate.spawn(): Creates a new worker isolate and executes the supplied entry-point function.

receivePort.first: Waits for the first message from the worker.

isolate.kill(): Stops the worker isolate if it is still running.

This example performs one calculation and receives one result. For a real long-lived worker, you would normally keep the isolate and its ports alive, listen for multiple messages, and implement a clear shutdown process.

10. Flutter Isolates vs Async/Await

Many Flutter developers confuse isolates with asynchronous programming.

Both help with application responsiveness, but they solve different problems.

Async/Await

The async and await keywords make asynchronous operations easier to write and manage.

They are commonly used for:

  • API requests.
  • Database operations.
  • File operations supported by asynchronous APIs.
  • Waiting for futures.
  • Handling asynchronous responses.

Example:

Future<void> fetchData() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/users'),
  );

  print(response.body);
}

The application can continue processing other events while waiting for the network response.

However, async and await do not automatically move CPU-heavy synchronous calculations to another isolate.

Flutter Isolates

Isolates allow CPU-intensive computations to run independently from the main isolate.

They are useful for:

  • Large JSON parsing.
  • Image processing.
  • Complex calculations.
  • Heavy data transformations.

Comparison

Async/AwaitFlutter Isolates
Manages asynchronous operationsRuns independent computations
Useful for network requestsUseful for CPU-heavy work
Does not automatically create another isolateUses a separate isolate on supported native platforms
Uses futuresUses message passing and futures
Great for waiting on I/OGreat for expensive computations

In many applications, you can use both together. For example, use await to fetch an API response and compute() to parse a large JSON response.

You can also read our tutorials on Flutter Async and Await and Flutter FutureBuilder.

11. Common Mistakes When Using Flutter Isolates

Isolates are useful, but developers can make mistakes when implementing them.

Here are some common issues to avoid.

1. Using Isolates for Small Tasks

Not every operation needs a separate isolate.

Creating an isolate has overhead, so moving a simple calculation or small JSON response to another isolate may not improve performance.

Use isolates when profiling shows that CPU-intensive work is affecting UI responsiveness.

2. Trying to Update Widgets from a Worker Isolate

A worker isolate cannot directly update Flutter widgets or use the main isolate’s widget state.

Instead, return the result to the main isolate and update the UI there.

3. Accessing Flutter UI APIs in a Worker Isolate

Flutter UI operations are associated with the main isolate.

A worker isolate should not try to build widgets, access widget state, or use UI-only APIs.

Some Flutter resources, such as rootBundle, are also unavailable in ordinary spawned isolates.

4. Sending Unsupported Objects Between Isolates

Isolates communicate through messages.

Some objects, including open sockets and certain resource handles, cannot be sent between isolates.

Prefer simple transferable values such as:

  • Strings.
  • Numbers.
  • Booleans.
  • Lists and maps containing transferable values.
  • SendPort objects.

5. Creating Too Many Isolates

Creating a new isolate for every small task can increase memory usage and overhead.

For one-time operations, use Isolate.run() or compute().

For repeated work, consider maintaining a long-lived worker isolate.

6. Ignoring Errors and Cleanup

Always consider what happens if an isolate fails or a computation throws an exception.

For long-lived workers, close ports and stop isolates when they are no longer needed.

12. Best Practices for Flutter Isolates

Follow these practices when using isolates in production applications:

  1. Use isolates for CPU-intensive work: Move expensive computations away from the main isolate when they cause UI jank.
  2. Prefer Isolate.run() for one-time tasks: It provides a simple way to execute a computation in another isolate.
  3. Use compute() for simple Flutter background tasks: It is convenient for passing one input to a function and receiving a result.
  4. Use Isolate.spawn() for long-running workers: It is useful when the same worker needs to process multiple messages.
  5. Keep messages simple: Send only the data the worker needs.
  6. Avoid unnecessary data copying: Large messages can add memory and processing overhead. Consider whether the data can be reduced or transferred efficiently.
  7. Keep UI updates on the main isolate: Return processed data and update widgets from the main isolate.
  8. Handle errors properly: Use error handling and appropriate cleanup for worker isolates.
  9. Profile your application: Use Flutter DevTools to identify actual performance bottlenecks before introducing isolates.
  10. Consider platform differences: Dart isolates are not supported on Flutter web in the same way as native platforms. The compute() function runs on the current event loop on web.

13. Frequently Asked Questions (FAQs)

What are Flutter Isolates?

Flutter Isolates are independent execution environments provided by Dart. They have separate memory and event loops and communicate through message passing. They help run CPU-intensive tasks without blocking the main UI isolate.

Why should we use isolates in Flutter?

You should use isolates when heavy computations affect application performance. Common examples include parsing large JSON responses, processing images, filtering large datasets, and performing complex calculations.

What is the difference between Isolate.run() and compute()?

Isolate.run() is a Dart API for executing a one-time computation in a new isolate. compute() is a Flutter convenience function that accepts a callback and a message. On native platforms, compute() uses an isolate; on web, it runs on the current event loop.

Does async/await create a new isolate?

No. The async and await keywords manage asynchronous operations using futures. They do not automatically move CPU-intensive synchronous work to another isolate.

Can isolates share variables in Dart?

No. Isolates have separate memory and do not share mutable state. They communicate by sending messages through ports.

Can we use isolates in Flutter web?

Dart web platforms do not support isolates in the same way as native platforms. Flutter’s compute() function is available, but it executes the computation on the current event loop on web rather than creating a separate isolate.

Can we use Flutter plugins inside an isolate?

Plugin support in worker isolates has limitations. Some platform-channel operations can be configured in background isolates, but not every plugin supports them. Always check the plugin’s documentation before using it outside the main isolate.

Is Isolate.run() better than Isolate.spawn()?

Neither is universally better. Isolate.run() is simpler for one-time computations because the isolate automatically exits after returning its result. Isolate.spawn() is more suitable for long-lived workers that need to process multiple messages.

Conclusion

Flutter Isolates are an important feature of Dart that helps developers build responsive and performant applications.

When an application performs CPU-intensive operations on the main isolate, it can cause UI jank and make the application feel slow. By moving those computations to worker isolates, you can keep the main isolate available for UI-related work.

For one-time computations, Isolate.run() and Flutter’s compute() function are convenient choices. For long-running background workers that need to communicate repeatedly, Isolate.spawn(), SendPort, and ReceivePort provide more control.

Start by identifying performance bottlenecks in your application, then use isolates where they provide a meaningful improvement.

With a good understanding of Flutter Isolates, you can handle large datasets, complex calculations, and other expensive operations more efficiently.

Leave a Comment