60fps in Flutter beibehalten: Auslagern von schweren JSON/KI-Berechnungen mit Isolate.run und Worker Pools

Flutter arbeitet grundlegend mit einem Single-Thread-Modell (Main Isolate). Das bedeutet, dass das UI-Rendering, die Ereignisbehandlung und deine gesamte Geschäftslogik auf demselben Thread ausgeführt werden. Wenn du also schwere Berechnungen durchführst, wie z.B. das Parsen großer JSON-Dateien oder die Ausführung von KI-Modellinferenz im Hauptthread, friert der Bildschirm ein oder ruckelt, was allgemein als ‘Jank’ bezeichnet wird.
Um dieses Problem zu lösen und eine flüssige Umgebung mit 60fps (oder 120fps) beizubehalten, musst du rechenintensive Aufgaben in einen separaten Thread auslagern. In Dart wird dieses Konzept als Isolate bezeichnet. In diesem Artikel untersuchen wir, wie du mit Isolate.run Threads für einfache Aufgaben leicht trennen kannst und wie du einen Worker Pool für sich wiederholende Aufgaben aufbaust.
Isolate.run für einmalige schwere Aufgaben
Mit der Einführung von Isolate.run in Dart 2.15 ist das Starten eines separaten Threads und das Abrufen seines Ergebnisses unglaublich einfach geworden. Schauen wir uns ein Beispiel an, bei dem wir eine große JSON-Antwort, die über das Netzwerk empfangen wurde, in eine Liste von Objekten parsen.
import 'dart:convert';
import 'dart:isolate';
// Datenmodell für die Konvertierung
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']);
}
}
// Funktion, die im Hauptthread ausgeführt wird
Future<List<HeavyData>> fetchAndParseData(String jsonString) async {
// Parsing-Logik im separaten Isolate ausführen
return await Isolate.run(() {
final List<dynamic> parsed = jsonDecode(jsonString);
return parsed.map((e) => HeavyData.fromJson(e)).toList();
});
}
Der Codeblock innerhalb von Isolate.run läuft in einem brandneuen Isolate. Sobald die Berechnung abgeschlossen ist, wird das Ergebnis sicher an das Haupt-Isolate zurückgegeben und das temporäre Isolate wird sofort beendet. Du erhältst eine saubere Thread-Trennung, ohne komplexe Ports (Ports) konfigurieren zu müssen.
Worker Pool Pattern für wiederkehrende Aufgaben
Da Isolate.run bei jedem Aufruf ein neues Isolate erstellt und zerstört, kann dies zu einem gewissen Overhead führen. Wenn deine App kontinuierlich kurze, aber schwere sich wiederholende Aufgaben auslöst, wie z.B. On-Device KI-Inferenz oder Bildfilterung, ist es viel effizienter, ein Worker Pool Muster zu verwenden, bei dem Isolates im Voraus erstellt und wiederverwendet werden.
Implementierung eines Worker Pools
Wir verwenden Ports aus dem Paket isolate, um einen Kommunikationskanal zwischen dem Haupt-Isolate und dem Hintergrund-Isolate aufzubauen.
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);
// Empfängt den SendPort als erste Nachricht
_sendPort = await receivePort.first as SendPort;
_isReady = true;
}
// Einstiegspunkt, der im Hintergrund ausgeführt wird
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];
// Führe schwere Aufgabe aus (Beispiel)
final result = data.toString().toUpperCase();
// Ergebnis zurückgeben
replyPort.send(result);
}
});
}
// Funktion zum Anfordern von Arbeit aus dem Hauptthread
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();
}
}
Wenn du dieses Worker-Objekt nun als Singleton oder Provider beibehältst, kannst du die Methode compute jederzeit aufrufen, wenn du sie brauchst. Dies spart die Kosten für das wiederholte Erstellen von Threads und hilft, unterbrechungsfreie 60fps-Animationen aufrechtzuerhalten.
Fazit
Wenn Leistungsprobleme in einer Flutter-App auftreten, ist die Ursache fast immer eine synchrone Berechnung, die den Hauptthread blockiert.
- Verwende für einmaliges Laden oder Parsen aktiv das intuitive
Isolate.run. - Für kontinuierlich auftretende Berechnungen empfehlen wir, manuell einen
Worker Poolzu konfigurieren oder entsprechende Pakete von pub.dev zu nutzen.
Überlasse die schwere Arbeit separaten Threads und entwirf deine App so, dass sich der Flutter-Hauptthread ausschließlich auf das Rendern der Benutzeroberfläche konzentrieren kann!