Adiós Chromium: Por qué migramos de Electron a Flutter (y cuándo no deberías hacerlo)

La semana pasada, nuestro equipo de ingeniería se enfrentó a un ultimátum: o reducíamos el consumo de memoria de nuestra aplicación de escritorio de monitoreo en tiempo real, o perdíamos el contrato con un cliente corporativo importante. La queja era clara: "Su agente en segundo plano consume más RAM que el propio sistema operativo". Nuestra aplicación estaba construida sobre Electron. En máquinas con 8GB de RAM, tener tres instancias de Chromium abiertas (Slack, VS Code y nuestra App) era insostenible.

El Problema de la Arquitectura: Chromium vs Skia

Para entender por qué ocurría esto, hay que bajar al nivel de arquitectura. En nuestro entorno de producción (Windows 11 y macOS Ventura, procesadores Intel i7 y M2), Electron empaqueta una versión completa de Chromium y Node.js. Esto significa que cada ventana es esencialmente una pestaña de navegador pesada. Incluso con una aplicación "Hola Mundo", la huella de memoria base rara vez baja de 120MB.

Por otro lado, Flutter compila a código máquina nativo (ARM64 o x64) y utiliza su propio motor de renderizado (Skia, y ahora Impeller en versiones recientes de 2025). No hay puente de JavaScript ni DOM que manipular. El canvas se pinta directamente.

El síntoma en logs: [Process: Renderer] Fatal Error: Allocation failed - JavaScript heap out of memory
Esto ocurría frecuentemente en nuestras instancias de Electron cuando procesábamos grandes volúmenes de datos JSON en el proceso de renderizado en lugar del proceso principal.

El Intento Fallido: Optimización de Electron

Antes de decidir reescribir todo, intentamos salvar la nave de Electron. Implementamos v8-compile-cache para mejorar el tiempo de inicio y movimos toda la lógica pesada a Web Workers y procesos hijos de Node.js. Intentamos usar librerías nativas de Rust compiladas como addons de Node para reducir la carga del Garbage Collector de JS.

¿El resultado? El rendimiento de la CPU mejoró, pero el consumo de memoria base seguía siendo el talón de Aquiles. Cada proceso Helper de Electron añadía un overhead fijo que no podíamos eliminar. La arquitectura multiproceso de Chromium, que es excelente para la seguridad y estabilidad de un navegador, es excesiva para una utilidad de sistema ligera.

La Solución: Migración a Flutter y FFI

Decidimos prototipar el módulo central en Flutter. La diferencia crítica no solo fue el renderizado, sino cómo nos comunicamos con el sistema nativo. En Electron, usábamos ipcMain e ipcRenderer, lo que implica serialización de mensajes (overhead). En Flutter, utilizamos dart:ffi (Foreign Function Interface) para llamar a APIs de C/C++ directamente sin serialización costosa.

Aquí hay un ejemplo de cómo cambió nuestra lógica de comunicación con el sistema operativo. Noten la diferencia en la complejidad de la "fontanería".

// 1. Enfoque Electron (JavaScript/Node)
// Requiere contexto de aislamiento y serialización JSON
const { ipcMain } = require('electron');
const os = require('os');

ipcMain.handle('get-system-stats', async () => {
  // Serialización implícita aquí puede ser costosa con objetos grandes
  return {
    memory: os.freemem(),
    cpus: os.cpus(),
    uptime: os.uptime()
  };
});

// 2. Enfoque Flutter (Dart + FFI)
// Acceso directo a memoria, CERO serialización para tipos primitivos
import 'dart:ffi' as ffi;
import 'package:ffi/ffi.dart';

// Definición de la firma C
typedef SystemFreeMemC = ffi.Int64 Function();
typedef SystemFreeMemDart = int Function();

void getMemoryUsage() {
  // Carga dinámica de la librería del sistema (ejemplo conceptual)
  final dylib = ffi.DynamicLibrary.open('system_api.so');
  
  final SystemFreeMemDart freeMem = dylib
      .lookup<ffi.NativeFunction<SystemFreeMemC>>('get_free_memory')
      .asFunction();

  // Llamada síncrona ultrarrápida (menos de 1ms)
  final int memory = freeMem();
  print('Memoria libre: $memory bytes');
}

En el ejemplo de Flutter anterior, aunque el código parece más verboso debido a la configuración de FFI, la ejecución es instantánea. No hay puente asíncrono obligatorio ni cola de eventos bloqueada por el renderizado de la UI. Para nuestra aplicación de monitoreo, esto eliminó el "jank" (tirones) de la interfaz.

Nota de Experiencia: Al usar MethodChannel en Flutter (la alternativa a FFI más común), todavía existe serialización. Sin embargo, el codec binario de Dart es significativamente más eficiente que el JSON.stringify que a menudo ocurre implícitamente en puentes JS mal optimizados.

Resultados del Benchmark (Producción)

Tras 3 semanas de refactorización del módulo core, lanzamos una versión beta interna. Las métricas fueron capturadas en una instancia AWS EC2 t3.medium (Windows Server) y una MacBook Air M1 base.

Métrica Electron (v28) Flutter (v3.19+) Mejora
Tamaño del Instalador 145 MB 38 MB ~73% Menor
RAM (Idle) 180 MB 32 MB ~82% Menor
Tiempo de Inicio (Cold) 2.4s 0.6s 4x Más rápido
Uso CPU (Background) 1.5% - 4% 0.1% - 0.5% Significativa

Los números hablan por sí solos. La reducción del tamaño del instalador fue un beneficio secundario inesperado, muy agradecido por nuestros usuarios con conexiones limitadas. La reducción de RAM fue el factor decisivo. Flutter gestiona la memoria como una aplicación nativa real, liberando recursos agresivamente cuando la ventana está minimizada, algo que Electron lucha por hacer debido a la gestión de memoria de V8.

Cuándo NO usar Flutter (Edge Cases)

No quiero pintar un cuadro donde Flutter es perfecto. Durante la migración encontramos obstáculos serios que deben considerar antes de abandonar Electron.

  1. Ecosistema de Librerías Node.js: Si tu aplicación depende fuertemente de librerías específicas de Node (como `sharp` para procesamiento de imágenes complejo o clientes de bases de datos oscuros), perderás acceso directo a npm. Dart tiene `pub.dev`, pero el ecosistema es un 10% del tamaño de npm.
  2. Renderizado de Texto y Accesibilidad: Aunque ha mejorado masivamente en 2025, el renderizado de fuentes en Flutter sobre escritorio a veces se siente ligeramente "diferente" al nativo del sistema operativo, mientras que Electron usa la pila web estándar que es píxel-perfecta en tipografía.
  3. SEO y Web Fallback: Si planeas reutilizar el código para una versión Web, recuerda que Flutter Web usa Canvas Kit (WASM). No es HTML/CSS real. El SEO es inexistente y el tiempo de carga inicial es mayor que una SPA de React/Electron.
Ver Guía Oficial de Instalación de Flutter
Advertencia de Code Push: Electron permite actualizar la lógica JS "en caliente" (hot code push) saltándose las tiendas de aplicaciones en algunos casos. Flutter compila a binario. Para actualizar, el usuario debe descargar el nuevo ejecutable o usar soluciones complejas como `shorebird`.

Conclusión

La batalla entre Flutter y Electron no es sobre cuál es "mejor", sino sobre qué recursos estás dispuesto a sacrificar. Si tu prioridad es la velocidad de desarrollo y tienes un equipo de desarrolladores web, Electron sigue siendo el rey. Pero si, como nosotros, te enfrentas a restricciones de hardware, necesitas rendimiento "cercano al metal" y quieres una experiencia de usuario fluida a 60/120fps garantizados, Flutter es la opción madura en 2025.

Para nuestro caso de uso de agente en segundo plano, la migración redujo nuestros tickets de soporte relacionados con "lentitud del PC" en un 90%. El esfuerzo de aprender Dart valió cada byte de RAM ahorrado.

Post a Comment