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.
Evan Emran
Mobile Developer & Tech Blogger

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:
- Creates a temporary isolate.
- Sends your image bytes to it.
- Executes the function.
- Returns the result.
- 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()
| Feature | compute() | 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:
Uint8ListStringintdoubleboolListMap
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.