Maintain 60fps in Flutter: Offloading Heavy JSON/AI Computations with Isolate.run and Worker Pools

Flutter fundamentally operates on a single-thread (Main Isolate) model. This means UI rendering, event handling, and your business logic all run on the same thread. Because of this, if you perform heavy computations like parsing massive JSON payloads or running AI model inference on the main thread, the screen will freeze or stutter, causing what is commonly known as ‘Jank’.
To solve this problem and maintain a buttery-smooth 60fps (or 120fps) environment, you must offload heavy tasks to a separate thread. In Dart, this concept is called an Isolate. In this article, we’ll explore how to easily separate threads using Isolate.run for simple tasks, and how to build a robust Worker Pool for repetitive computations.
Isolate.run for One-off Heavy Tasks
With the introduction of Isolate.run in Dart 2.15, spawning a separate thread and retrieving its result has become incredibly simple. Let’s look at an example of parsing a large JSON payload received over the network into a list of objects.
import 'dart:convert';
import 'dart:isolate';
// Data model for conversion
class HeavyData {
final int id;
final String content;
HeavyData({required this.id, required this.content});
factory HeavyData.fromJson(Map<String, dynamic> json) {
return HeavyData(id: json['id'], content: json['content']);
}
}
// Function executed on the main thread
Future<List<HeavyData>> fetchAndParseData(String jsonString) async {
// Execute parsing logic in a separate Isolate
return await Isolate.run(() {
final List<dynamic> parsed = jsonDecode(jsonString);
return parsed.map((e) => HeavyData.fromJson(e)).toList();
});
}
The code block inside Isolate.run runs in a brand new Isolate. Once the computation finishes, the result is safely passed back to the main Isolate, and the temporary Isolate is immediately terminated. You get clean thread separation without having to configure complex Ports.
Worker Pool Pattern for Repetitive Tasks
Because Isolate.run creates and destroys a new Isolate every time it is called, it can introduce overhead. If your app continuously triggers short but heavy repetitive tasks, such as on-device AI inference or image filtering, it is much more efficient to use a Worker Pool pattern where Isolates are created in advance and reused.
Implementing a Worker Pool
We use ports from the isolate package to establish a communication channel between the main Isolate and the background Isolate.
import 'dart:isolate';
class ComputationWorker {
late SendPort _sendPort;
late Isolate _isolate;
bool _isReady = false;
Future<void> init() async {
final receivePort = ReceivePort();
_isolate = await Isolate.spawn(_workerEntry, receivePort.sendPort);
// Receive the SendPort as the first message
_sendPort = await receivePort.first as SendPort;
_isReady = true;
}
// Entry point executed in the background
static void _workerEntry(SendPort mainSendPort) {
final workerReceivePort = ReceivePort();
mainSendPort.send(workerReceivePort.sendPort);
workerReceivePort.listen((message) {
if (message is List) {
final SendPort replyPort = message[0];
final dynamic data = message[1];
// Perform heavy computation (example)
final result = data.toString().toUpperCase();
// Return result
replyPort.send(result);
}
});
}
// Function to request work from the main thread
Future<dynamic> compute(dynamic data) async {
if (!_isReady) throw Exception('Worker is not ready');
final responsePort = ReceivePort();
_sendPort.send([responsePort.sendPort, data]);
final result = await responsePort.first;
responsePort.close();
return result;
}
void dispose() {
_isolate.kill();
}
}
Now, by keeping this Worker object as a singleton or provider, you can call the compute method whenever needed. This saves the cost of creating threads repeatedly and helps maintain uninterrupted 60fps animations.
Conclusion
When performance issues arise in a Flutter app, the root cause is almost always synchronous computations blocking the main thread.
- For one-off loading or parsing, actively use the intuitive
Isolate.run. - For continuously occurring computations, we recommend configuring a
Worker Poolmanually or leveraging related packages on pub.dev.
Leave the heavy lifting to separate threads, and design your app so the main Flutter thread can focus exclusively on rendering the UI!