DEV Community

vmodal_ai
vmodal_ai

Posted on

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

Image processing is a common requirement in Flutter applications. You might need to compress photos, apply filters, resize images, or run OCR before uploading them to a server.

A common mistake beginners make is performing these heavy operations on the main thread, causing the UI to freeze. Users may experience lag, unresponsive buttons, or even Android warnings such as:

Skipped 120 frames! The application may be doing too much work on its main thread.
Enter fullscreen mode Exit fullscreen mode

Fortunately, Flutter provides Isolates, which allow you to move CPU-intensive work to a background thread while keeping the UI smooth and responsive.

In this tutorial, you'll learn what Isolates are, why they matter, and how to use them with a practical image processing example.


What is an Isolate?

An Isolate is a separate Dart execution environment with its own memory.

Unlike traditional threads, isolates do not share memory. Instead, they communicate by sending messages.

Think of it like this:

Main Isolate
│
├── Handles UI
├── Responds to user input
├── Draws widgets
│
└──────────────┐
               │
               ▼
      Background Isolate
      ├── Resize image
      ├── Compress image
      ├── Apply filters
      └── OCR processing
Enter fullscreen mode Exit fullscreen mode

The main isolate remains responsive while the background isolate performs the heavy work.


Why Use Isolates?

Without isolates:

  • Scrolling becomes laggy.
  • Animations stutter.
  • Buttons stop responding.
  • Loading indicators freeze.
  • Android may report skipped frames.

With isolates:

  • Smooth animations
  • Responsive UI
  • Better user experience
  • Faster image processing
  • Heavy computations run in the background

Step 1: Create a New Flutter Project

flutter create isolate_demo
Enter fullscreen mode Exit fullscreen mode

Open the project.

cd isolate_demo
Enter fullscreen mode Exit fullscreen mode

Step 2: Add the Image Package

Open pubspec.yaml.

dependencies:
  flutter:
    sdk: flutter

  image: ^4.2.0
Enter fullscreen mode Exit fullscreen mode

Install dependencies.

flutter pub get
Enter fullscreen mode Exit fullscreen mode

The image package lets us decode, resize, and manipulate images in pure Dart.


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;
Enter fullscreen mode Exit fullscreen mode

Notice this import:

import 'package:flutter/foundation.dart';
Enter fullscreen mode Exit fullscreen mode

It provides the compute() function, which creates a temporary isolate for background work.


Step 4: Load an Image

Imagine the user has selected an image from the gallery.

final bytes = await File(imagePath).readAsBytes();
Enter fullscreen mode Exit fullscreen mode

Decode it.

final image = img.decodeImage(bytes)!;
Enter fullscreen mode Exit fullscreen mode

Now we have an editable image object.


Step 5: Resize the Image (Without Isolates)

Let's resize the image directly.

final resized = img.copyResize(
  image,
  width: 1200,
);
Enter fullscreen mode Exit fullscreen mode

This works...

But if the image is very large (for example, 6000×4000 pixels), the UI may freeze while resizing.


Step 6: Create a Background Function

Instead of processing on the main thread, we'll move the work to another isolate.

Create 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),
  );
}
Enter fullscreen mode Exit fullscreen mode

Notice that this function is outside any widget class.

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


Step 7: Run the Function in an Isolate

Instead of calling the function directly:

resizeImage(bytes);
Enter fullscreen mode Exit fullscreen mode

Use:

final resizedBytes = await compute(
  resizeImage,
  bytes,
);
Enter fullscreen mode Exit fullscreen mode

What happens?

User selects image
          │
          ▼
Main UI ──────────────► compute()
                         │
                         ▼
                 Background Isolate
                         │
                  Resize Image
                         │
                         ▼
               Return Result
                         │
                         ▼
                  Update UI
Enter fullscreen mode Exit fullscreen mode

The UI remains smooth while the image is processed in the background.


Step 8: Show a Loading Indicator

Always let users know that processing is in progress.

bool processing = false;
Enter fullscreen mode Exit fullscreen mode

Before starting:

setState(() {
  processing = true;
});
Enter fullscreen mode Exit fullscreen mode

After processing:

setState(() {
  processing = false;
});
Enter fullscreen mode Exit fullscreen mode

Display a loading spinner.

if (processing)
  const CircularProgressIndicator()
Enter fullscreen mode Exit fullscreen mode

The app now feels much more responsive.


Step 9: Display the Processed Image

Once the isolate finishes:

setState(() {
  processedImage = resizedBytes;
});
Enter fullscreen mode Exit fullscreen mode

Show it.

Image.memory(processedImage!)
Enter fullscreen mode Exit fullscreen mode

The image updates without freezing the screen.


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);
}
Enter fullscreen mode Exit fullscreen mode

This simple change moves all the heavy image processing off the main thread.


Why compute()?

Flutter provides two ways to use isolates:

compute()

  • Very easy to use
  • Perfect for one-time tasks
  • Automatically creates and destroys an isolate

Example:

await compute(
  resizeImage,
  bytes,
);
Enter fullscreen mode Exit fullscreen mode

Isolate.spawn()

  • More flexible
  • Better for long-running background tasks
  • Requires manual communication using SendPort and ReceivePort

Most image processing tasks are perfectly suited for compute().


When Should You Use Isolates?

Use isolates whenever a task is CPU-intensive, such as:

  • Image resizing
  • Image compression
  • Applying filters
  • OCR (Optical Character Recognition)
  • Face detection
  • Video frame processing
  • PDF generation
  • Large JSON parsing
  • Encryption and decryption
  • Machine learning inference

Avoid using isolates for simple operations like updating a variable or making HTTP requests, as network calls are already asynchronous and don't block the UI.


Common Mistakes

Doing Heavy Work in build()

Never process images inside the build() method.

The build() method may be called many times, leading to repeated work and poor performance.


Forgetting to Show Progress

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

Always display a loading indicator or progress overlay.


Passing Unsupported Objects

Data sent to an isolate must be transferable.

Good choices include:

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

Avoid passing complex widget objects or open file handles.


Using Isolates for Small Tasks

Creating an isolate has a small overhead.

If an operation only takes a few milliseconds, running it on the main isolate is usually sufficient.


Real-World Use Cases

Many production Flutter apps use isolates behind the scenes for:

  • Camera apps that apply filters before saving photos
  • Social media apps that compress images before uploading
  • Document scanners performing OCR
  • Barcode and QR code processing
  • AI applications running image inference
  • Photo editors with multiple filters
  • PDF and report generation
  • Offline image manipulation

Whenever you notice dropped frames or a sluggish interface during heavy computation, isolates are often the right solution.


Final Thoughts

Flutter Isolates are one of the best tools for improving app performance. By moving CPU-intensive work—such as image resizing, compression, or OCR—to a background isolate, you keep the main thread focused on rendering the interface and responding to user input.

For one-off tasks, compute() provides a simple and effective solution. As your applications grow, you can explore Isolate.spawn() for more advanced scenarios.

Keeping expensive computations off the main isolate is a key step toward building fast, responsive, and professional Flutter applications that provide an excellent user experience.

Top comments (0)