effidevFlutter · Edge de Cloudflare · Optimización de costes en la nube
Español

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

Optimización del rendimiento de la app Flutter: Separación de hilos con Isolates

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.

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.