← Volver al blog

Por qué reescribimos nuestra app Electron en Tauri/Rust

engineering tauri rust

Por qué dejamos atrás Electron

GitSquid v1 estaba construido sobre Electron. Funcionaba. Los usuarios podían explorar repositorios, preparar cambios, gestionar ramas, manejar remotos y usar un terminal integrado -- todo desde una única aplicación de escritorio. Internamente, nos apoyábamos en simple-git para las operaciones Git, node-pty y xterm.js para el terminal, y el stack habitual de React para la interfaz.

Pero a medida que la aplicación maduraba, las contrapartidas de Electron se volvían más difíciles de ignorar. Habíamos lanzado un producto funcional, pero el propio runtime trabajaba en nuestra contra. Así que reescribimos todo el backend en Rust usando Tauri 2.x -- manteniendo el 95 % de nuestro frontend React intacto.

Esta es la historia de por qué lo hicimos, cómo lo hicimos y qué se rompió en el camino.

El problema con Electron

Electron es un framework probado. VS Code, Slack, Discord -- grandes productos funcionan con él. Pero para un cliente Git, la sobrecarga es desproporcionada respecto a la tarea.

  • Tamaño de la app: Electron incluye un navegador Chromium completo y un runtime Node.js. Nuestra aplicación empaquetada pesaba más de 150 MB. Para un cliente Git, eso es difícil de justificar.
  • Uso de memoria: Cada ventana de Electron genera múltiples procesos de Chromium. El consumo de memoria en reposo estaba muy por encima de lo que una aplicación nativa necesitaría para el mismo trabajo.
  • Tiempo de arranque: Lanzar Chromium no es instantáneo. Los usuarios notaban un retraso perceptible antes de que la aplicación fuera interactiva, especialmente en hardware antiguo.
  • Sensación nativa: A pesar de los esfuerzos de Electron con barras de título y menús nativos, la aplicación nunca terminaba de sentirse como parte del sistema operativo. Pequeños detalles -- gestión de ventanas, comportamiento del foco, atajos de teclado -- siempre tenían un ligero sabor a aplicación web.

Ninguno de estos problemas es un factor decisivo por sí solo. Pero combinados, creaban una fricción que no se alineaba con lo que una herramienta de desarrollo debería transmitir: rapidez, ligereza e invisibilidad.

Por qué Tauri 2.x

Evaluamos varias alternativas antes de decidirnos por Tauri. Los factores decisivos fueron claros:

  • Webview del sistema en lugar de Chromium incluido. Tauri usa la webview proporcionada por el sistema operativo (WebKit en macOS, WebView2 en Windows, WebKitGTK en Linux). Solo esto elimina la mayor parte del tamaño binario de Electron.
  • Backend en Rust. El backend se ejecuta como un binario Rust compilado. Seguridad de memoria, sin recolector de basura, rendimiento predecible. Para una herramienta que ejecuta docenas de operaciones Git por interacción, esto importa.
  • Binarios drásticamente más pequeños. Una aplicación Tauri puede distribuirse en menos de 10 MB. Compárelo con 150 MB+.
  • Multiplataforma con integración nativa. Tauri 2.x proporciona APIs sólidas para menús nativos, bandeja del sistema, notificaciones y diálogos de archivos. La aplicación se siente como parte del sistema operativo.
  • Ecosistema activo y buena experiencia de desarrollo. El sistema de plugins de Tauri, el modelo IPC y la documentación han madurado significativamente con la versión 2.x. Estaba listo para uso en producción.

La estrategia de migración

Una reescritura completa es arriesgada. Mitigamos ese riesgo con una clara separación de responsabilidades.

Mantener el frontend, reescribir el backend

Nuestro frontend React ya estaba estructurado en torno a un límite IPC. Los componentes llamaban a funciones que se comunicaban con el proceso principal de Electron; solo necesitábamos redirigir esas llamadas a la API invoke de Tauri. Aproximadamente el 95 % del código React se trasladó sin cambios.

El backend fue otra historia. Cada servicio Node.js -- operaciones Git, vigilancia de archivos, gestión del terminal, autenticación, configuración -- tuvo que ser reimplementado en Rust. En total, escribimos aproximadamente 7.200 líneas de Rust y migramos más de 120 comandos IPC.

Git CLI en lugar de libgit2

Una pregunta frecuente: ¿por qué no usar git2-rs (los bindings de Rust para libgit2)?

Elegimos std::process::Command para llamar directamente a la Git CLI. Las razones:

  • libgit2 no soporta todas las funcionalidades de Git. Operaciones como rebase interactivo, algunas estrategias de merge y ciertas opciones de configuración faltan o se comportan de manera diferente.
  • Los usuarios ya tienen Git instalado. Al llamar a su binario Git local, garantizamos paridad de comportamiento con la línea de comandos. Si funciona en el terminal, funciona en GitSquid.
  • La depuración es más simple. Los comandos que ejecutamos son los mismos que un usuario escribiría. Cuando algo falla, los mensajes de error son familiares.

Reemplazando el ecosistema Node.js

Cada dependencia de Node.js fue reemplazada por un equivalente en Rust:

  • simple-git se convirtió en llamadas directas a la Git CLI vía std::process::Command
  • node-pty se convirtió en portable-pty para la emulación de terminal
  • El almacenamiento de tokens se trasladó al crate keyring, que usa el llavero del sistema operativo (macOS Keychain, Windows Credential Manager, Linux Secret Service)
  • La vigilancia de archivos cambió al crate notify
  • Las peticiones HTTP se trasladaron a reqwest

Migración de configuración transparente

Mantuvimos la misma estructura de directorio de configuración. Los usuarios que actualizan de v1 a v2 no pierden sus ajustes, repositorios guardados ni preferencias. El backend Rust lee los mismos archivos de configuración que escribió el backend Node.js.

Desafíos que enfrentamos

La migración no fue un camino de rosas. Varios problemas nos costaron un tiempo significativo de depuración.

Bytes nulos en el parsing de Git log

Usamos cadenas de formato personalizadas con git log --format para parsear datos de commits. En la versión Electron, usábamos caracteres separadores específicos entre campos. Al portar a Rust, inicialmente intentamos usar bytes nulos (\x00) como separadores de campo -- parecían una elección segura ya que nunca aparecerían en mensajes de commit.

Error. Los strings de Rust manejan bytes nulos sin problema, pero varias capas del stack los tratan como terminadores de cadena al estilo C. La salida del log se truncaba silenciosamente en el primer byte nulo. Cambiamos a un delimitador de múltiples caracteres que nunca ocurriría naturalmente en la salida de Git.

Almacenamiento de tokens en el llavero

El crate keyring almacena credenciales en el llavero del sistema operativo, que es el enfoque correcto. Pero nos encontramos con un problema similar de bytes nulos: estábamos concatenando datos de tokens con separadores de bytes nulos antes de almacenarlos. Al recuperarlos, algunas implementaciones del llavero del sistema operativo truncaban el valor en el primer byte nulo, devolviendo credenciales incompletas. La solución fue la misma -- usar un separador visible e inequívoco.

Complejidad de la autenticación en BitBucket

GitHub y GitLab tienen flujos de autenticación basados en tokens relativamente sencillos. BitBucket es más complejo. Dependiendo de la operación y el tipo de cuenta, podrías necesitar un nombre de usuario, un correo electrónico, una contraseña de aplicación o un token OAuth -- y las reglas de cuál usar en cada caso no siempre son obvias. Conseguir que la autenticación de BitBucket funcionara de manera fiable para clone, fetch, push y llamadas a la API requirió varias iteraciones.

Staging por líneas

GitSquid soporta el staging de líneas individuales de un diff, no solo hunks completos. En la versión Electron, la generación de patches se hacía en JavaScript. Para la versión Tauri, trasladamos esta lógica al backend Rust. Generar un formato de patch Git válido a partir de selecciones arbitrarias de líneas, manejar casos límite con líneas de contexto y asegurar que git apply acepte el resultado -- esta fue una de las partes más intrincadas de la reescritura. La implementación en Rust terminó siendo más robusta que la versión original en JavaScript.

Apóstrofos franceses rompiendo builds

Este fue memorable. Algunas de nuestras cadenas de texto visibles para el usuario contenían apóstrofos tipográficos franceses (la variedad curva, U+2019) en lugar de apóstrofos ASCII rectos. Estos causaron problemas de codificación durante el proceso de build en ciertas configuraciones de CI. La solución fue trivial una vez identificada, pero los mensajes de error no apuntaban ni remotamente al problema real.

Resultados

Tras completar la migración y alcanzar paridad completa de funcionalidades, estos son nuestros resultados:

  • Tamaño de la app: significativamente reducido comparado con el build de Electron. El paquete Tauri es una fracción de los 150 MB+ que distribuíamos antes.
  • Uso de memoria: línea base más baja y más predecible bajo carga. Sin árbol de procesos Chromium consumiendo RAM en segundo plano.
  • Tiempo de arranque: notablemente más rápido. La aplicación es interactiva casi de inmediato.
  • Paridad de funcionalidades: 100 %. Cada funcionalidad de v1 existe en v2. El terminal integrado, staging por líneas, soporte multi-remote, gestión de ramas -- todo.
  • Reutilización del frontend: el 95 % del código React se trasladó, validando nuestra decisión arquitectónica de mantener un límite IPC limpio.

¿Valió la pena?

Sí.

La reescritura requirió un esfuerzo significativo. Aproximadamente 7.200 líneas de Rust, más de 120 comandos IPC para reimplementar y varios bugs no obvios que solo salieron a la superficie porque Rust y Node.js manejan los casos límite de manera diferente. No fue un proyecto de fin de semana.

Pero el resultado es una aplicación fundamentalmente mejor. GitSquid v2 arranca más rápido, usa menos memoria, se distribuye como un binario más pequeño y se siente más nativo en cada plataforma. El backend Rust es más predecible y más fácil de razonar que el equivalente en Node.js. Y como mantuvimos el mismo frontend React, los usuarios ven la misma interfaz que ya conocen.

Si mantienes una aplicación Electron y la sobrecarga del runtime te molesta, Tauri es una opción seria. El ecosistema es lo suficientemente maduro para uso en producción, y la ruta de migración -- especialmente si ya tienes una separación limpia entre frontend y backend -- es más manejable de lo que una reescritura completa podría sugerir.

La idea clave: trata tu límite IPC como un contrato de API. Si tu frontend solo sabe cómo hacer invoke('get_commits', { path, branch }) y no le importa si la respuesta viene de Node.js o Rust, cambiar el backend se convierte en un proyecto realista en lugar de una fantasía.