Blog de Obi Madu
Volver a todos los artículos
DesktopSystem DesignInfrastructure

Conoce Tauri [2.0]

Cómo Tauri construye aplicaciones de escritorio a partir de tecnologías web, dónde sitúa el límite de confianza en comparación con Electron, y el cambio de mentalidad de una página web en una ventana a una aplicación nativa.

Conoce Tauri [2.0]

La primera vez que alguien me describió Tauri, dijo que construyes una aplicación de escritorio usando HTML, CSS y JavaScript. Mi reacción fue escepticismo. Eso suena como una página web en una ventana, no a una aplicación real. Luego señal que VS Code, Slack y Discord están todos construidos de la misma manera, usando Electron, que es la versión más antigua y popular de este enfoque. Esas son aplicaciones reales, usadas por millones de personas cada día. La arquitectura tiene un nombre: aplicaciones de escritorio híbridas. No es un hack, y no es nueva.

Funciona así:

Capa de UI (tecnologías web)

    Puente nativo

    Capacidades del OS

Tu interfaz está construida con la plataforma web. Un puente la conecta al sistema operativo. El puente es lo que convierte una página web en una aplicación de escritorio.

Electron demostró que esto funciona

Probablemente has usado aplicaciones Electron sin darte cuenta. VS Code, Slack, Discord y una larga lista de otras herramientas de escritorio están construidas con ello. Electron tomó la posición de que empaquetar un motor de navegador completo dentro de cada app era un precio razonable por permitir a los desarrolladores web lanzar software de escritorio sin aprender un stack nuevo.

HTML / CSS / JS

Puente Chromium + Node

      OS

El tradeoff es el tamaño. Cada app de Electron envía su propia copia de Chromium y Node. Una pequeña aplicación Electron puede ocupar 150 megabytes en disco porque contiene un navegador entero. El uso de memoria sigue el mismo patrón: cada ventana de Electron trae su propio proceso Chromium.

Tauri toma la misma filosofía pero hace tradeoffs diferentes:

HTML / CSS / JS

   Tauri IPC

    Rust

     OS

En lugar de empaquetar un navegador, Tauri usa el WebView que ya está instalado en el sistema del usuario. En Windows es WebView2, en macOS es WKWebView, en Linux es WebKitGTK. En lugar de Node, el backend es Rust.

ElectronTauri
Motor de navegadorChromium empaquetadoWebView del sistema
BackendNode.jsRust
Tamaño de la appGrande (100+ MB)Pequeño (típico 2-10 MB)
Modelo de seguridadNode tiene acceso total al sistemaEl frontend no es confiable por defecto
MemoriaMayor (Chromium completo por app)Menor (comparte WebView del sistema)

La diferencia de tamaño es lo que la mayoría de la gente nota primero. El modelo de seguridad es la distinción más interesante, y voy a llegar a ello.

Un proceso, no dos

Cuando empecé a leer sobre Tauri, seguía buscando la parte donde el frontend se conecta al backend. Llevaba el modelo web en la cabeza, donde el navegador habla con un servidor sobre HTTP, y asumí que Tauri tendría algo similar. Una API de localhost, quizás, o algún tipo de servidor embebido con el que hablara el WebView.

Eso no es lo que ocurre.

Cuando ejecutas:

pnpm tauri dev

arranca un ejecutable de Rust. Ese ejecutable hace cuatro cosas:

  1. Crea una ventana nativa del OS
  2. Crea un WebView dentro de esa ventana
  3. Carga tu frontend en el WebView
  4. Crea un puente IPC entre el WebView y el código Rust
Ejecutable de Rust
├── ventana nativa
├── WebView (tu app React/Vue/Svelte)
└── puente IPC

No hay un servidor frontend separado. No hay una API de localhost. No hay un proceso de backend separado. El proceso de Rust es el orquestador. Crea la ventana, carga la UI web en ella y espera para manejar mensajes de ella.

Esa fue la parte con la que tuve que quedarme un rato. En una aplicación web, el navegador y el backend son dos programas separados corriendo en máquinas distintas. El navegador descubre el backend a través de una URL. En Tauri son un programa. El "backend" es simplemente el código Rust corriendo en el mismo proceso que creó el WebView en primer lugar. Si has usado Next.js, esto se parece más a eso. Tu frontend y tus rutas de API son un proyecto, un proceso, con el framework gestionando el enrutamiento. No despliegas un frontend y un backend separados. Tauri tiene la misma forma.

Cómo JavaScript habla con Rust

Viniendo del desarrollo web, esperarías simplemente importar algo y llamarlo. Así es como funciona todo en JavaScript. Instalas un paquete, lo importas, lo usas.

Tauri te da un helper que se ve exactamente así:

import { invoke } from "@tauri-apps/api/core";

const result = await invoke("greet", { name: "John" });

Y en la superficie, eso es lo que es. Llamas a algo, recibes un resultado. Pero por debajo, nada de esto funciona como reconocerías de la web. No hay un módulo compartido. No hay una función importada. Estás enviando un mensaje a otro proceso y esperando a que responda.

JavaScript
   |
   | mensaje: { command: "greet", args: { name: "John" } }
   |
   puente IPC
   |
   despachador de comandos de Rust
   |
   fn greet(name: String) -> String
   |
   | devuelve: "Hello, John"
   |
Promise de JavaScript se resuelve

En el lado de Rust, la función que recibe esto se ve así:

#[tauri::command]
fn greet(name: String) -> String {
    format!("Hello, {}", name)
}

El atributo #[tauri::command] registra la función con el despachador IPC. Cuando JavaScript llama invoke("greet", ...), el despachador encuentra esta función, pasa los argumentos y devuelve el resultado. El lado de JavaScript obtiene una Promise. El lado de Rust ejecuta una función compilada real.

El nombre para esto es IPC, Comunicación entre Procesos. La razón por la que existe es que dos procesos no pueden alcanzar directamente la memoria del otro. Tienen que comunicarse a través de mensajes. Lo que ves arriba es una forma estructurada de que dos procesos intercambien datos sin compartir memoria.

Hubo una parte de esto que me tropezó. En una aplicación web, el navegador habla con el backend sobre HTTP, lo que significa que necesita un puerto, algo como localhost:3000. En Tauri, no hay puerto. Seguía buscando dónde se establecía la conexión y no podía encontrarla, porque no hay conexión. El puente IPC es creado internamente por el runtime de Tauri. Es como dos habitaciones en el mismo edificio. No llamas a alguien por teléfono cuando está en la habitación de al lado. Usas el tubo de correo interno.

El puente ya está ahí

Una vez que entendí que no había puerto, la siguiente pregunta era obvia: si no hay servidor, ¿cómo encuentra JavaScript a Rust?

La respuesta es que no lo encuentra. No tiene que hacerlo.

Cuando el proceso de Rust crea el WebView, inyecta el puente de comunicación en él. Conceptualmente:

Runtime de Rust
   |
   | inyecta
   |
window.__TAURI__

Cuando tu JavaScript llama invoke(), usa ese puente. El frontend no descubre a Rust. No hay una URL a la que apuntar, ningún puerto al que conectarse. Rust construyó el entorno en el que el frontend corre, y el canal de comunicación era parte de ese entorno antes de que se cargara la primera línea de JavaScript.

Esa es una diferencia clara del modelo web. En la web, el navegador descubre el backend a través de una URL. En Tauri, el backend creó el entorno en el que el frontend corre, y el canal de comunicación era parte de ese entorno desde el principio.

Dónde vive el límite de confianza

El frontend no es confiable. Eso es cierto para cada frontend, y no es la parte interesante. La parte interesante es dónde cada framework pone el límite de confianza y qué te permite scopear.

En Electron, el backend de Node tiene acceso total al sistema por defecto. Si el frontend obtiene ese acceso es una bandera de configuración, nodeIntegration. Ponla en true y tu renderer puede llamar child_process, fs, cualquier cosa que Node pueda hacer. Ponla en false y el renderer está en sandbox, pero el proceso principal sigue teniendo acceso total, y típicamente expones lo que necesitas a través de ipcMain.handle. El límite es "¿tiene el frontend la API de Node o no?" Es una puerta binaria. No hay un sistema de capacidades que diga que el frontend puede llamar exportReport pero no deleteFile. Escribes esa lógica tú mismo en cada manejador IPC.

En Tauri, el frontend tiene acceso cero por defecto. Cada comando que el frontend puede llamar debe estar explícitamente registrado como un #[tauri::command], y el sistema de capacidades controla qué comandos puede invocar el frontend. Solo el lado de Rust tiene acceso al OS. El sistema de capacidades scopea lo que el frontend puede pedir. No escribes un guard en cada manejador. El framework lo hace cumplir.

ElectronTauri
Acceso al OS del backendTotal por defectoTotal, pero solo Rust lo tiene
Acceso del frontend al OSBandera de configuración (nodeIntegration)Ninguno por defecto
Lo que el frontend puede llamarLo que expongas vía ipcMain.handleSolo comandos registrados con #[tauri::command]
Scoping por comandoEscribes la lógica del guard tú mismoEl sistema de capacidades scopea cada comando

La analogía del restaurante sigue vigente. El personal del comedor pone pedidos en una vía de tickets. La cocina decide qué hacer y cómo hacerlo. Pero en Electron, la puerta de la cocina tiene cerradura, y tienes que recordar cerrarla y decidir quién recibe una llave. En Tauri, la puerta de la cocina empieza cerrada, y cada plato específico que el comedor puede pedir tiene que ser añadido al menú explícitamente. El valor predeterminado es "nada está en el menú." Añades elementos uno a la vez, y el framework lo hace cumplir.

El build

Durante el desarrollo, dos cosas corren al mismo tiempo:

Frontend: Vite dev server (hot reload, fast refresh)
Backend:  Cargo (compilador de Rust, vigilando cambios)

Editas tus componentes React, Vite los recarga en caliente. Editas tu código Rust, Cargo recompila. La ventana nativa se mantiene abierta mientras ambos lados se actualizan.

En producción, el frontend se construye y Rust se compila en un binario de release. Los dos se empaquetan juntos:

Build del frontend
      +
Build de release de Rust
      |
      v
Aplicación nativa

El output depende del objetivo:

PlataformaOutput
Windows.exe / .msi
macOS.app / .dmg
LinuxAppImage / .deb
Android.apk / .aab
iOS.ipa

Tauri móvil vale la pena mencionarlo aquí. En móvil, la arquitectura es la misma: tu UI web corre en el WebView de la plataforma, Android WebView o iOS WKWebView, y Rust se compila nativamente para la plataforma móvil. No es React Native. No hay puente de JavaScript a UI nativa. La UI es un WebView, y Rust maneja el lado nativo.

Tu app de React puede correr en un navegador normal con pnpm dev. Pero invoke() no funcionará, porque no hay runtime de Rust ni puente IPC. La función existe en el paquete de JavaScript, pero cuando intenta enviar un mensaje a través del puente, no hay nada al otro lado.

En Tauri, el runtime de Rust está corriendo, el puente existe, y invoke() lleva mensajes a funciones reales de Rust. Esa es la diferencia entre abrir tu app en Chrome y correrla a través de Tauri. El código es el mismo. El runtime es diferente.

Cuándo encaja

Tauri funciona bien para:

  • Apps de productividad de escritorio
  • Herramientas de desarrollador
  • Software empresarial
  • Apps para tomar notas
  • Utilidades de archivos
  • Herramientas internas de empresa
  • Utilidades multiplataforma que necesitan correr en Windows, macOS y Linux

Es menos ideal para:

  • Juegos 3D de alto rendimiento
  • Aplicaciones pesadas en gráficos donde necesitas cada detalle de UI nativa
  • Apps que necesitan integración profunda específica de plataforma con frameworks de UI a nivel OS

Los buenos encajes comparten algo: la interfaz es el trabajo, y el sistema operativo proporciona capacidades de apoyo como acceso a archivos, notificaciones y gestión de ventanas. Los malos encajes son lo contrario: los gráficos o el sistema de UI de la plataforma nativa es el trabajo, y una UI web se interpone.

El cambio de mentalidad

La pregunta con la que empecé fue si realmente estamos diseñando interfaces en lugar de apps de escritorio. La respuesta es sí, pero el enmarcado importa.

No estás poniendo una página web en una ventana de escritorio. Estás construyendo una aplicación nativa donde la tecnología de UI resulta ser la plataforma web. Esa distinción es lo que hace funcionar la arquitectura. El lado de Rust es un backend nativo real con acceso real al OS. El lado web es la capa de presentación. El puente entre ellos es donde vive el límite de confianza.

Una página web en una ventana sería un navegador apuntando a una URL, sin capacidades nativas. Lo que Tauri te da es una aplicación nativa compilada que resulta renderizar su interfaz con tecnologías web. La diferencia es el backend de Rust, el puente IPC y el modelo de seguridad a su alrededor.