Por qué construimos ChainSolve con Rust y WebAssembly
Un análisis técnico en profundidad sobre por qué elegimos Rust compilado a WebAssembly para el motor de cálculo de ChainSolve, y las mejoras de rendimiento que logramos.
Cuando nos propusimos construir ChainSolve, teníamos un requisito innegociable: cada cálculo debe ejecutarse en el navegador con un rendimiento casi nativo. Sin viajes redondos al servidor. Sin dependencias en la nube. Sin datos que salgan de la máquina del usuario. Este requisito nos llevó a Rust y WebAssembly.
El Requisito de Rendimiento
ChainSolve permite a los usuarios construir gráficos de cálculo con potencialmente miles de bloques, cada uno realizando una operación matemática sobre sus entradas y pasando resultados a bloques aguas abajo. Cuando un usuario cambia un valor de entrada, todo el subgráfico aguas abajo debe recalcularse y actualizarse en tiempo real — idealmente dentro de un único fotograma de animación (16ms a 60fps).
Una implementación pura en JavaScript del motor de cálculo llegó a su límite alrededor de 200 bloques antes de que las caídas de fotogramas se hicieran evidentes. Necesitábamos al menos una mejora de 10x para soportar nuestro objetivo de 5.000+ bloques por gráfico.
¿Por qué Rust?
Evaluamos varios lenguajes que se compilan a WebAssembly: C, C++, Go y Rust. Elegimos Rust por tres razones.
Seguridad de Memoria sin Recolección de Basura
El sistema de propiedad de Rust garantiza la seguridad de la memoria en tiempo de compilación sin un recolector de basura. Esto es crítico para WebAssembly porque el modelo de memoria lineal de Wasm significa que un uso después de liberación o un desbordamiento de búfer no solo bloquea tu pestaña — corrompe todo el espacio de memoria silenciosamente. Rust elimina estos errores por construcción.
Abstracciones sin Costo
Las abstracciones de Rust (genéricos, iteradores, coincidencia de patrones) se compilan al mismo código máquina que escribirías a mano en C. Esto significa que podemos escribir código limpio y expresivo sin pagar un costo de rendimiento.
Excelente Cadena de Herramientas de WebAssembly
La herramienta wasm-pack, el crate wasm-bindgen y el ecosistema más amplio de Rust-a-Wasm son maduros y bien documentados. Podemos exponer funciones de Rust a JavaScript con un mínimo de boilerplate y sin código FFI manual.
Descripción General de la Arquitectura
El motor de cálculo de ChainSolve tiene tres capas:
-
Step [counter(steps)] Representación de Gráfico
El gráfico de cálculo se almacena como una lista de adyacencia con ordenamiento topológico. Cuando la estructura del gráfico cambia (bloques añadidos, eliminados o reconectados), recomputamos el orden topológico usando el algoritmo de Kahn. Esto determina la secuencia de evaluación.
-
Step [counter(steps)] Motor de Evaluación
El motor recorre el orden topológico y evalúa cada bloque. Los bloques se implementan como objetos trait de Rust con una interfaz común
evaluate(&self, inputs: &[Value]) -> Result<Value, EvalError>. Esto nos permite añadir nuevos tipos de bloques sin modificar el motor. -
Step [counter(steps)] Puente JavaScript
La capa de puente usa
wasm-bindgenpara exponer una API mínima a JavaScript:create_graph(),add_block(),connect(),set_input(),evaluate()yget_results(). Los datos se serializan como JSON para tipos complejos y se pasan como matrices f64 sin procesar para datos numéricos para minimizar la sobrecarga de serialización.
Puntos de Referencia
Comprobamos el motor Wasm contra una implementación JavaScript pura equivalente en tres cargas de trabajo:
| Carga de Trabajo | JS (ms) | Wasm (ms) | Aceleración |
|---|---|---|---|
| 100 bloques, cadena lineal | 2.1 | 0.18 | 11.7x |
| 1.000 bloques, gráfico ramificado | 34.2 | 2.4 | 14.3x |
| 5.000 bloques, DAG complejo | 412.0 | 22.1 | 18.6x |
La aceleración aumenta con la complejidad del gráfico porque el motor Wasm se beneficia de una mejor localidad de caché y la ausencia de comprobaciones de tipo dinámico de JavaScript.
Integración de Web Worker
El motor de cálculo se ejecuta dentro de un Web Worker para evitar bloquear el hilo principal. El patrón de comunicación es directo:
- El hilo principal envía mutaciones de gráfico (bloque añadido, conexión cambiada, entrada actualizada) como mensajes estructurados al Worker.
- El Worker aplica mutaciones al motor Wasm y dispara re-evaluación.
- Los resultados se envían de vuelta al hilo principal vía
postMessage. - Para conjuntos de resultados grandes, usamos
SharedArrayBuffercon Atomics para evitar el costo de serialización depostMessage.
Esta arquitectura significa que el hilo de UI nunca se bloquea por cálculo. Incluso si un gráfico tarda 500ms en evaluarse, el usuario puede continuar arrastrando bloques, desplazando el lienzo e interactuando con la interfaz sin ningún tartamudeo.
Lecciones Aprendidas
Construir una aplicación Wasm de producción nos enseñó varias lecciones:
-
El tamaño del binario Wasm importa. Nuestra compilación inicial fue de 2.4MB. Después de habilitar LTO,
wasm-opty eliminar símbolos de depuración, la redujimos a 340KB comprimido. Cada kilobyte cuenta para el tiempo de carga inicial. -
Evita asignaciones pequeñas frecuentes. El asignador de Wasm es más simple que un asignador nativo. Usamos asignación de arena para datos temporales por evaluación, lo cual redujo la sobrecarga de asignación en un 60%.
-
Perfila en el navegador, no en la línea de comandos. Las características de rendimiento difieren entre Rust nativo y Wasm. El panel Performance de Chrome DevTools con soporte de perfilado de Wasm fue nuestra herramienta de optimización principal.
-
Prueba ambos lados del puente. Los errores en la serialización de
wasm-bindgenson sutiles. Mantenemos un conjunto completo de pruebas que se ejecuta tanto en Rust nativo (víacargo test) como en el navegador (víawasm-pack test).
Conclusión
Rust y WebAssembly han sido la opción correcta para el motor de cálculo de ChainSolve. La combinación de seguridad de memoria, abstracciones sin costo y rendimiento casi nativo en el navegador nos permite entregar una experiencia de cálculo visual en tiempo real que no sería posible con JavaScript solo.
Si estás considerando Wasm para tu próxima aplicación basada en navegador, esperamos que este artículo proporcione contexto útil para tu decisión. Y si quieres ver los resultados en acción, prueba a construir un gráfico de cálculo en app.chainsolve.co.uk.