- El parser WASM escrito en Rust es estructuralmente rápido, pero la copia de datos y la sobrecarga de serialización en el límite entre JS y WASM terminaron siendo el cuello de botella de rendimiento
- Devolver objetos directamente mediante
serde-wasm-bindgenfue entre 9 y 29% más lento que la serialización JSON, debido al costo de conversiones detalladas entre runtimes - Al portar todo el pipeline a TypeScript, se logró en la misma arquitectura un rendimiento por llamada entre 2.2 y 4.6 veces mayor
- En procesamiento por streaming, una mejora de O(N²)→O(N) mediante caché incremental por oración permitió obtener una velocidad total de procesamiento entre 2.6 y 3.3 veces mayor
- En conclusión, WASM es adecuado para cargas intensivas de cómputo y llamadas poco frecuentes, pero no es adecuado para parseo de objetos JS ni para funciones invocadas con mucha frecuencia
Estructura y límites del parser WASM en Rust
- El parser
openui-langestá compuesto por un pipeline de 6 etapas que convierte el DSL generado por un LLM en un árbol de componentes de React- Etapas:
autocloser → lexer → splitter → parser → resolver → mapper → ParseResult - Cada etapa realiza tokenización, análisis sintáctico, resolución de variables, transformación del AST, etc.
- Etapas:
- El código Rust en sí es rápido, pero en cada llamada ocurre el proceso de copiar cadenas entre JS↔WASM, serializar JSON y deserializarlo
- Copia de la cadena de entrada (JS→WASM), parseo interno en Rust, serialización del resultado a JSON, copia del JSON (WASM→JS), deserialización en JS
- Esa sobrecarga en el límite entre ambos entornos terminó dominando el rendimiento total; la velocidad de cómputo de Rust no era el cuello de botella
El intento con serde-wasm-bindgen y por qué falló
- Para evitar la serialización JSON, se aplicó
serde-wasm-bindgen, que devuelve estructuras de Rust directamente como objetos JS - Sin embargo, se observó que era 30% más lento
- JS no puede leer directamente la memoria de una estructura Rust, y como el layout de memoria difiere entre runtimes, se requiere una conversión campo por campo
- En cambio, la serialización JSON genera una sola cadena dentro de Rust, que luego JS procesa con un
JSON.parsealtamente optimizado
- Resultados del benchmark
Fixture JSON round-trip serde-wasm-bindgen Cambio simple-table 20.5µs 22.5µs -9% contact-form 61.4µs 79.4µs -29% dashboard 57.9µs 74.0µs -28%
Cambio a TypeScript y mejora de rendimiento
- Se hizo un port completo a TypeScript de la misma estructura de 6 etapas, eliminando el límite con WASM y ejecutando todo directamente dentro del heap de V8
- Resultados del benchmark por llamada individual
Fixture TypeScript WASM Mejora de velocidad simple-table 9.3µs 20.5µs 2.2x contact-form 13.4µs 61.4µs 4.6x dashboard 19.4µs 57.9µs 3.0x - Solo con eliminar WASM, el costo por llamada cayó de forma notable, aunque la ineficiencia de la estructura de streaming seguía presente
El problema O(N²) del parseo por streaming y su mejora
- Cuando la salida del LLM llega en varios chunks, se producía una ineficiencia O(N²) al volver a parsear cada vez toda la cadena acumulada
- Ejemplo: un documento de 1000 caracteres parseado 50 veces en bloques de 20 caracteres → 25,000 caracteres procesados en total
- Como solución, se introdujo una caché incremental por oración
- Las oraciones completas se guardan en caché, y solo se vuelve a parsear la oración en curso
- El AST cacheado y el AST nuevo se combinan para devolver el resultado
- Benchmark sobre el stream completo
Fixture TS ingenuo TS incremental Mejora de velocidad simple-table 69µs 77µs Ninguna contact-form 316µs 122µs 2.6x dashboard 840µs 255µs 3.3x - Cuantas más oraciones haya, mayor es el efecto de la caché, y el throughput total mejora de forma lineal
Lecciones sobre el uso de WASM
- Casos adecuados
- Tareas intensivas en cómputo y con poca interacción: procesamiento de imagen y video, criptografía, simulación física, códecs de audio, etc.
- Portabilidad de librerías nativas existentes: SQLite, OpenCV, libpng, etc.
- Casos no adecuados
- Parseo de texto estructurado hacia objetos JS: el costo de serialización domina
- Funciones con entradas cortas y llamadas frecuentes: el costo del límite entre entornos supera al cómputo
- Lecciones clave
- Hay que perfilar dónde está el cuello de botella antes de elegir el lenguaje
- El paso directo de objetos con
serde-wasm-bindgenes más costoso - Mejorar la complejidad algorítmica puede ser más efectivo que cambiar de lenguaje
- WASM y JS no comparten el heap, y el costo de conversión siempre existe
Resultado final: con el cambio a TypeScript y la caché incremental se logró una mejora de rendimiento de 2.2 a 4.6 veces por llamada, y de 2.6 a 3.3 veces en el stream completo
2 comentarios
¿No habrá sido más bien con la intención de tirarle indirectas a un artículo muy técnico sobre optimización de rendimiento en Rust..?
Comentarios en Hacker News
El verdadero punto clave no es TypeScript frente a Rust, sino la corrección del algoritmo de streaming al reducirlo de O(N²) a O(N)
Solo este cambio, basado en caché a nivel de sentencias (
statement), ya dio una mejora de 3.3xIndependientemente del lenguaje elegido, desde la perspectiva del usuario, el principal factor de mejora en la latencia fue esta parte
Da la impresión de que el título subestima este interesante punto de ingeniería
El artículo en sí es interesante, pero últimamente ya estoy cansado de los títulos excesivamente diseñados para generar clics
Se trata de medir el tiempo de cada llamada y usar la mediana (
median), pero en navegadores, donde los motores JS tienen defensas contra ataques por temporización, dudo de la precisiónDecir “reescribimos código del lenguaje L a M y se volvió más rápido” es algo esperable
Porque fue una oportunidad para corregir decisiones enredadas y erróneas, y aplicar un mejor enfoque que apareció después
De hecho, aunque L=M, la mejora de velocidad suele venir no del lenguaje sino del proceso de reescritura y rediseño
Investigué más a fondo intentando mejorar el rendimiento de la serialización de objetos en la frontera entre Rust y JS
El enfoque de serde no parecía bueno en términos de rendimiento, y resumí un intento de mejorarlo en mi entrada de blog
Me preguntaba por qué Open UI no estaba haciendo trabajo relacionado con WASM
Pero luego me confundí porque esta nueva empresa usa el nombre Open UI
El grupo original, Open UI W3C Community Group, lleva más de 5 años creando estándares como popover de HTML,
selectpersonalizable, invoker command y accordionRealmente están haciendo un gran trabajo
Dicen que integraron serde-wasm-bindgen en el intento de “saltarse el viaje de ida y vuelta por JSON”, pero al final parece reinventar JSON en forma binaria
Hoy en día el JSON de V8 ya está muy optimizado, y implementaciones como simdjson pueden procesar gigabytes por segundo
No creo que JSON sea el cuello de botella
Me gustó muchísimo el diseño de ese blog
En particular, la barra lateral tipo ‘scrollspy’ que resalta los encabezados según la posición de desplazamiento me pareció genial
Según me dijo Claude, parece que fue hecho con fumadocs.dev
No me quedó claro cuál era exactamente el propósito del parser Rust WASM
Esa parte no estaba clara en el artículo y necesitaba más explicación
Esto parece buscar evitar la filtración de información causada por prompt injection
El parser compila los fragmentos transmitidos por streaming desde el LLM para construir una UI en tiempo real
Antes reiniciaban el parser desde cero con cada fragmento, pero al cambiarlo a un método de procesamiento incremental (durante el port de Rust a TypeScript), el rendimiento mejoró mucho
Tenía la duda de si TypeScript hoy en día no corre sobre una base de Golang
En broma, pero quizá si lo reescriben otra vez en Rust haya otra mejora de rendimiento de 3x /s