Mantener 60fps en Flutter: Delegando Computaciones Pesadas de JSON/IA con Isolate.run y Worker Pools

Flutter funciona fundamentalmente con un modelo de un solo hilo (Main Isolate). Esto significa que la renderización de la interfaz de usuario, el manejo de eventos y toda la lógica de tu negocio se ejecutan en el mismo hilo. Por lo tanto, si realizas operaciones pesadas como parsear grandes payloads JSON o ejecutar inferencia de modelos de IA en el hilo principal, la pantalla se congelará o tartamudeará, causando lo que comúnmente se conoce como ‘Jank’.
Para resolver este problema y mantener un entorno de 60fps (o 120fps) fluido, debes separar las tareas pesadas en un hilo distinto. En Dart, a esto se le llama Isolate. En este artículo, exploraremos cómo separar hilos fácilmente utilizando Isolate.run para tareas simples, y cómo construir un Worker Pool para cálculos repetitivos.
Isolate.run para tareas pesadas únicas
Con la introducción de Isolate.run en Dart 2.15, crear un hilo separado y recuperar su resultado se ha vuelto increíblemente sencillo. Veamos un ejemplo en el que parseamos un gran JSON recibido por red en una lista de objetos.
import 'dart:convert';
import 'dart:isolate';
// Modelo de datos para la conversión
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']);
}
}
// Función ejecutada en el hilo principal
Future<List<HeavyData>> fetchAndParseData(String jsonString) async {
// Ejecuta la lógica de parseo en un Isolate separado
return await Isolate.run(() {
final List<dynamic> parsed = jsonDecode(jsonString);
return parsed.map((e) => HeavyData.fromJson(e)).toList();
});
}
El bloque de código dentro de Isolate.run se ejecuta en un Isolate nuevo. Una vez que finaliza la computación, el resultado se devuelve de forma segura al Isolate principal, y el Isolate temporal se termina de inmediato. Obtienes una separación de hilos limpia sin tener que configurar Puertos (Ports) complejos.
Patrón Worker Pool para tareas repetitivas
Debido a que Isolate.run crea y destruye un Isolate nuevo cada vez que se le llama, puede generar cierta sobrecarga. Si tu aplicación desencadena continuamente tareas cortas pero repetitivas y pesadas, como la inferencia de IA en el dispositivo o el filtrado de imágenes, es mucho más eficiente usar un patrón Worker Pool donde los Isolates se crean de antemano y se reutilizan.
Implementando un Worker Pool
Utilizamos puertos del paquete isolate para establecer un canal de comunicación entre el Isolate principal y el Isolate en segundo plano.
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);
// Recibe el SendPort como el primer mensaje
_sendPort = await receivePort.first as SendPort;
_isReady = true;
}
// Punto de entrada ejecutado en segundo plano
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];
// Ejecuta la tarea pesada (ejemplo)
final result = data.toString().toUpperCase();
// Devuelve el resultado
replyPort.send(result);
}
});
}
// Función para solicitar trabajo desde el hilo principal
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();
}
}
Ahora, manteniendo este objeto Worker como un singleton o provider, puedes llamar al método compute siempre que lo necesites. Esto ahorra el costo de crear hilos repetidamente y ayuda a mantener animaciones fluidas a 60fps sin interrupciones.
Conclusión
Cuando surgen problemas de rendimiento en una app Flutter, la causa suele ser operaciones síncronas que bloquean el hilo principal.
- Para cargas o parseos únicos, utiliza activamente la intuitiva función
Isolate.run. - Para cálculos continuos, te recomendamos configurar un
Worker Poolmanualmente o utilizar paquetes relacionados de pub.dev.
Deja el trabajo pesado para hilos separados y diseña tu aplicación para que el hilo principal de Flutter se dedique exclusivamente a renderizar la interfaz de usuario.