← Back to all articles
FlutterAug 3, 2026 · 9 min read

Flutter Isolates: Speed Up Heavy Image Processing Without Freezing the UI

Image processing is a common requirement in modern Flutter applications. Whether you're building a social media platform, a document scanner, an e-commerce app, or a photo editor, chances are you'll need to process images before displaying or uploading them.

E

Evan Emran

Mobile Developer & Tech Blogger

Flutter Isolates: Speed Up Heavy Image Processing Without Freezing the UI

Some common image processing tasks include:

  • Compressing images before upload
  • Resizing large photos
  • Applying filters or effects
  • Cropping images
  • Performing OCR (Optical Character Recognition)
  • Running AI or ML inference on images
  • Generating thumbnails

A common mistake many Flutter developers make is performing these CPU-intensive operations directly on the main isolate (UI thread). While the processing is happening, Flutter cannot render frames or respond to user interactions.

The result?

  • Scrolling becomes sluggish
  • Buttons stop responding
  • Animations freeze
  • Loading indicators stop spinning
  • Android may show warnings like:
Skipped 120 frames! The application may be doing too much work on its main thread.

Fortunately, Flutter provides Isolates, allowing heavy computations to run in the background without affecting the user interface.

In this article, you'll learn:

  • What Isolates are
  • Why they're important
  • When to use compute()
  • When to use Isolate.spawn()
  • How the newer Isolate.run() simplifies background work
  • A complete image processing example
  • Best practices and common mistakes

What is an Isolate?

An Isolate is an independent Dart execution environment with its own memory and event loop.

Unlike traditional threads in many programming languages, Isolates do not share memory. Instead, they communicate by passing messages.

This design eliminates many common multithreading problems such as:

  • Race conditions
  • Deadlocks
  • Shared memory synchronization
  • Mutexes and locks

Instead of accessing shared variables, isolates send immutable or transferable data to each other.

Think of it like this:

Main Isolate
│
├── Handles UI
├── Responds to user input
├── Draws widgets
│
└──────────────┐
               │
               ▼
      Background Isolate
      ├── Resize image
      ├── Compress image
      ├── Apply filters
      ├── OCR
      └── AI Processing

The main isolate remains responsive while the background isolate performs the expensive computation.


Why Use Isolates?

Without isolates, heavy CPU work blocks the UI.

Without Isolates

  • Frozen UI
  • Laggy scrolling
  • Dropped animation frames
  • Unresponsive buttons
  • Poor user experience
  • Android skipped frame warnings

With Isolates

  • Smooth animations
  • Responsive interface
  • Background processing
  • Better battery efficiency
  • Improved user experience
  • Faster perceived performance

Step 1: Create a Flutter Project

flutter create isolate_demo

Move into the project.

cd isolate_demo

Step 2: Add the Image Package

Open pubspec.yaml.

dependencies:
  flutter:
    sdk: flutter

  image: ^4.2.0

Install dependencies.

flutter pub get

The image package is written entirely in Dart, making it compatible with isolates.


Step 3: Import the Required Packages

import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:image/image.dart' as img;

Notice this import:

import 'package:flutter/foundation.dart';

It provides the compute() function, which makes running one-off tasks in a background isolate extremely simple.


Step 4: Load an Image

Assume the user has selected an image.

final bytes = await File(imagePath).readAsBytes();

Decode it.

final image = img.decodeImage(bytes)!;

Now the image can be edited.


Step 5: Resize the Image (Without Isolates)

A simple resize operation looks like this:

final resized = img.copyResize(
  image,
  width: 1200,
);

Although this works perfectly for small images, resizing a large image (for example, 6000×4000 pixels) can take noticeable time.

During that period, the UI cannot update.


Step 6: Create a Background Function

Move the heavy work into a top-level function.

Uint8List resizeImage(Uint8List bytes) {
  final image = img.decodeImage(bytes)!;

  final resized = img.copyResize(
    image,
    width: 1200,
  );

  return Uint8List.fromList(
    img.encodeJpg(resized),
  );
}

Notice that the function is outside your widget class.

This is important because compute() can only execute top-level or static functions.


Step 7: Execute the Function Using compute()

Instead of:

resizeImage(bytes);

Use:

final resizedBytes = await compute(
  resizeImage,
  bytes,
);

Internally, Flutter:

  1. Creates a temporary isolate.
  2. Sends your image bytes to it.
  3. Executes the function.
  4. Returns the result.
  5. Destroys the isolate.

The workflow looks like this:

User Selects Image
        │
        ▼
 Main Isolate
        │
        ▼
    compute()
        │
        ▼
 Background Isolate
        │
        ▼
 Resize Image
        │
        ▼
 Return Bytes
        │
        ▼
 Update UI

Throughout the entire process, the UI remains responsive.


Step 8: Show a Loading Indicator

Background processing may still take several seconds.

Always inform users that work is in progress.

bool processing = false;

Before processing:

setState(() {
  processing = true;
});

After processing:

setState(() {
  processing = false;
});

Display a loading spinner.

if (processing)
  const CircularProgressIndicator()

This small improvement makes your app feel much more responsive.


Step 9: Display the Processed Image

Once the isolate finishes, update the UI.

setState(() {
  processedImage = resizedBytes;
});

Display it.

Image.memory(processedImage!)

The image appears without freezing the application.


Complete Example

import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:image/image.dart' as img;

Uint8List resizeImage(Uint8List bytes) {
  final image = img.decodeImage(bytes)!;

  final resized = img.copyResize(
    image,
    width: 1200,
  );

  return Uint8List.fromList(
    img.encodeJpg(resized),
  );
}

Future<void> processImage(Uint8List bytes) async {
  final result = await compute(
    resizeImage,
    bytes,
  );

  print(result.length);
}

With only a few changes, you've moved expensive image processing away from the UI thread.


Understanding Flutter's Three Main Isolate APIs

Flutter provides multiple ways to run code in another isolate. Choosing the right one depends on your use case.

1. compute()

compute() is the simplest and most commonly used solution.

Best for

  • Image resizing
  • Image compression
  • JSON parsing
  • OCR
  • Thumbnail generation
  • One-time background tasks

Example:

final result = await compute(
  resizeImage,
  bytes,
);

Advantages

  • Extremely simple
  • Automatically manages the isolate lifecycle
  • Less boilerplate
  • Great for most Flutter apps

Limitations

  • Only supports top-level or static functions
  • Creates a new isolate every time it's called

2. Isolate.spawn()

Isolate.spawn() provides complete control over isolate creation.

Unlike compute(), it does not automatically return a result. Instead, you must manually communicate using SendPort and ReceivePort.

Example:

final receivePort = ReceivePort();

await Isolate.spawn(
  imageProcessor,
  receivePort.sendPort,
);

Communication typically works like this:

Main Isolate
      │
 ReceivePort
      ▲
      │
 SendPort
      │
Background Isolate

Best for

  • Long-running workers
  • Continuous background processing
  • Background services
  • Processing multiple tasks with the same isolate
  • Streaming data between isolates

Advantages

  • Full control
  • Reusable isolate
  • Suitable for complex applications

Drawbacks

  • More boilerplate
  • Manual message passing
  • Manual isolate lifecycle management

3. Isolate.run()

Introduced in newer versions of Dart, Isolate.run() offers a cleaner way to execute a single asynchronous computation in a separate isolate.

Unlike Isolate.spawn(), you don't need to manually create SendPort or ReceivePort. It automatically creates an isolate, runs the callback, returns the result, and shuts the isolate down.

Example:

final resizedBytes = await Isolate.run(() {
  final image = img.decodeImage(bytes)!;

  final resized = img.copyResize(
    image,
    width: 1200,
  );

  return Uint8List.fromList(
    img.encodeJpg(resized),
  );
});

Best for

  • One-time CPU-intensive work
  • Cleaner Dart-only background tasks
  • Situations where you don't need the extra abstraction of compute()

Advantages

  • Less boilerplate than Isolate.spawn()
  • Automatically manages the isolate lifecycle
  • Returns values directly
  • No manual message passing

Limitations

  • Available only in newer Dart SDK versions
  • Doesn't provide the same convenience integration with Flutter's compute() helper for top-level functions

compute() vs Isolate.run() vs Isolate.spawn()

Featurecompute()Isolate.run()Isolate.spawn()
Easy to use
Manual message passing
Automatic cleanup
Returns values directly
Long-running workers
Best for one-time tasks
Full control

When Should You Use Isolates?

Use isolates whenever your application performs CPU-intensive work.

Examples include:

  • Image resizing
  • Image compression
  • Image filtering
  • OCR
  • Face detection
  • Barcode detection
  • QR code generation
  • PDF generation
  • Large JSON parsing
  • Encryption
  • Decryption
  • Video frame processing
  • Machine learning inference
  • Audio waveform generation

Avoid using isolates for operations like:

  • HTTP requests
  • Database queries
  • Updating variables
  • Simple calculations

These operations are already asynchronous or complete quickly enough that creating an isolate would add unnecessary overhead.


Common Mistakes

Processing Images Inside build()

Never perform expensive work inside the build() method.

Since Flutter may rebuild widgets many times, the same computation could run repeatedly, severely impacting performance.


Forgetting to Show Progress

If image processing takes several seconds, users may think the app has crashed.

Always display:

  • Loading indicators
  • Progress overlays
  • Skeleton screens
  • Progress percentages (if applicable)

Passing Unsupported Objects

Only transferable data can be sent between isolates.

Good choices include:

  • Uint8List
  • String
  • int
  • double
  • bool
  • List
  • Map

Avoid passing:

  • Widget instances
  • BuildContext
  • Open file handles
  • Controllers
  • Streams
  • Platform channels

Using Isolates for Tiny Tasks

Creating an isolate has a small performance cost.

If a task only takes a few milliseconds, it's often faster to execute it directly on the main isolate.


Real-World Use Cases

Many production Flutter applications rely on isolates behind the scenes.

Examples include:

  • Camera apps applying filters before saving
  • Social media apps compressing photos before upload
  • Document scanners performing OCR
  • AI-powered image classification
  • Barcode and QR code scanning
  • Offline photo editors
  • Video processing
  • Thumbnail generation
  • PDF creation
  • End-to-end encryption
  • Large JSON parsing from local files

Whenever you notice dropped frames or a sluggish interface during heavy computation, moving that work to an isolate is often the right solution.


Final Thoughts

Flutter Isolates are one of the most effective tools for improving application performance. By moving CPU-intensive work—such as image resizing, compression, OCR, or machine learning—to a background isolate, you allow the main isolate to focus on rendering the UI and responding to user interactions.

For most one-time tasks, compute() offers a clean and beginner-friendly API. If you're writing pure Dart code and want an even simpler approach, Isolate.run() is an excellent modern alternative that eliminates manual message passing. For advanced scenarios that require long-running workers, custom communication, or complete control over an isolate's lifecycle, Isolate.spawn() remains the most flexible option.

Choosing the right API depends on your use case, but the goal is always the same: keep expensive work off the main isolate so your Flutter applications remain fast, smooth, and enjoyable to use.