# Abandonar WASM: Cómo Reescribir un Analizador Rust en TypeScript Aumentó la Velocidad 3x
Cambiar de Rust/WASM a TypeScript para analizar un DSL personalizado en el navegador eliminó el sobrecoste de las fronteras de ejecución y permitió un algoritmo incremental, reduciendo el tiempo total de procesamiento de flujos en un 2,6–3,3x.
Arquitectura del Analizador openui-lang
El analizador convierte un DSL personalizado generado por un modelo de lenguaje (LLM) en un árbol de componentes React. El pipeline consta de seis etapas secuenciales, cada una con una tarea específica:
- Autocerrador — asegura la corrección sintáctica del texto parcial añadiendo el número mínimo de corchetes y comillas de cierre.
- Léxico — realiza un escaneo de una sola pasada del string de entrada para generar tokens tipados.
- Divisor — divide el flujo de tokens en instrucciones individuales
id = expresión. - Analizador — usa descenso recursivo para construir un árbol de sintaxis abstracta (AST) a partir de expresiones.
- Resolutor — resuelve referencias de variables, soporta elevación de declaraciones y detecta dependencias cíclicas.
- Mapeador — transforma el AST interno en el formato público
OutputNodelisto para renderizado en React.
Cada etapa es crítica para el rendimiento general, ya que el analizador se ejecuta en cada fragmento del flujo de salida del LLM.
El Problema: El Sobrecoste de la Frontera WASM–JS Domina
La implementación original en Rust compilada a WebAssembly no era lenta en el análisis en sí — estaba lastrada por los costes de transferencia de datos entre entornos de ejecución. Cada llamada implicaba:
- Copiar el string del heap de JS a la memoria lineal de WASM.
- Serializar el resultado del análisis a JSON dentro de Rust.
- Copiar el string JSON de vuelta al heap de JS.
- Deserializar el string en un objeto JS mediante
JSON.parse().
Probar serde-wasm-bindgen para transferencia directa de objetos sin serialización JSON empeoró el rendimiento en un 9–29%. Esto se debía a la materialización recursiva de estructuras de datos Rust en objetos JS nativos, provocando innumerables cruces finos de ejecución.
La Solución: Puerto Completo a TypeScript + Algoritmo Incremental
Abandonar WASM por completo en favor de TypeScript eliminó el sobrecoste de transferencia de datos. Pero el verdadero cambio fue optimizar para streaming con un enfoque incremental.
La versión ingenua reanalizaba todo el string acumulado desde cero en cada nuevo fragmento, lo que llevaba a una complejidad cuadrática O(N²) respecto al número de fragmentos.
Introdujimos una estrategia incremental con caché a nivel de instrucción:
- Las instrucciones que terminan en salto de línea con nivel de anidamiento cero se consideran inmutables.
- Sus AST se almacenan en caché tras el primer análisis.
- En nuevos fragmentos, solo se reanaliza la instrucción final (inacabada).
- El resultado final fusiona los AST en caché con el análisis fresco de la cola.
Esto redujo la complejidad a lineal O(L), donde L es la longitud total del documento.
Resultados de Benchmarks y Rendimiento
Probamos con tres casos realistas generados por LLM.
Llamada Única al Analizador (tiempo medio, microsegundos):
| Caso | TypeScript | WASM (vía JSON) | Mejora |
|--------------|------------|-----------------|---------|
| simple-table | 9,3 | 20,5 | 2,2x |
| contact-form | 13,4 | 61,4 | 4,6x |
| dashboard | 19,4 | 57,9 | 3,0x |
Coste Total de Procesamiento de Flujo (tiempo medio, microsegundos):
| Caso | TS Ingenuo (O(N²)) | TS Incremental (O(N)) | Mejora |
|--------------|---------------------|-----------------------|---------|
| simple-table | 69 | 77 | — |
| contact-form | 316 | 122 | 2,6x |
| dashboard | 840 | 255 | 3,3x |
Sin mejora en simple-table porque es una sola instrucción sin nada que cachear. Las ganancias escalan con el número de instrucciones.
Lecciones Clave
- En configuraciones Rust/WASM con llamadas frecuentes, el verdadero asesino no es el cómputo — es la copia de datos y serialización entre fronteras.
- El paso directo de objetos WASM-a-JS vía
serde-wasm-bindgenpuede ser más lento que un JSON optimizado debido a conversiones internas interminables. - Las mejoras algorítmicas (de O(N²) a O(N)) suelen superar microoptimizaciones de lenguaje o runtime.
- WASM brilla en tareas de baja interacción (procesado de imágenes), pero falla en análisis frecuentes a objetos JS complejos.
Cuándo Usar WASM (y Cuándo No)
✅ WASM Gana:
- Tareas intensivas en cómputo con entrada masiva y salida escalar/in situ (procesado multimedia, cripto, simulaciones físicas).
- Portar libs C/C++ existentes al navegador sin reescritura completa.
❌ WASM Pierde:
- Análisis frecuente de texto estructurado que necesita retornos de objetos JS complejos.
- Caminos calientes con entradas minúsculas donde el sobrecoste de frontera no se amortiza.
— Editorial Team
Aún no hay comentarios.