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.
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
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
Open 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 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;
Notice this import:
import 'package:flutter/foundation.dart';
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();
Decode it.
final image = img.decodeImage(bytes)!;
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,
);
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),
);
}
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);
Use:
final resizedBytes = await compute(
resizeImage,
bytes,
);
What happens?
User selects image
│
▼
Main UI ──────────────► compute()
│
▼
Background Isolate
│
Resize Image
│
▼
Return Result
│
▼
Update UI
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;
Before starting:
setState(() {
processing = true;
});
After processing:
setState(() {
processing = false;
});
Display a loading spinner.
if (processing)
const CircularProgressIndicator()
The app now feels much more responsive.
Step 9: Display the Processed Image
Once the isolate finishes:
setState(() {
processedImage = resizedBytes;
});
Show it.
Image.memory(processedImage!)
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);
}
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,
);
Isolate.spawn()
- More flexible
- Better for long-running background tasks
- Requires manual communication using
SendPortandReceivePort
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:
Uint8ListStringintdoubleListMap
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)