En 2022, cuando bajé una query de 40 segundos a 80ms con un índice compuesto, entendí algo que ningún tutorial me había enseñado: los sistemas de alto rendimiento no se construyen con más código, se construyen eliminando fricción. Ese día no agregué nada al sistema — agregué metadata, una estructura auxiliar que le permitía al motor de base de datos hacer menos trabajo. Hoy, cuando leo sobre cómo un equipo construyó un runtime de Rust para TypeScript, pienso en eso. No en el glamour de Rust. En la decisión de dónde poner la fricción.
Pero antes de arrancar: ya escribí sobre deadlocks y Surelock en Rust la semana pasada. Ya me quemé. Y también pasé varios posts trabajando sobre 9 patrones de TypeScript. Estoy sesgado en ambas direcciones. Lo reconozco. Aun así, voy a intentar leer esto lo más limpio que puedo.
Rust runtime TypeScript rendimiento: de qué estamos hablando
El proyecto en cuestión tomó el runtime de TypeScript — la capa que ejecuta tu código TS transpilado — y reemplazó partes críticas del pipeline con implementaciones en Rust. No es un transpilador nuevo. No es un compilador completo. Es una intervención quirúrgica: agarraron los cuellos de botella específicos del proceso de ejecución y los reimplementaron en un lenguaje sin garbage collector, con control de memoria manual y zero-cost abstractions.
El resultado que reportan: reducciones de latencia de hasta 10x en operaciones de I/O intensivas. Cold starts más rápidos. Menor footprint de memoria en lambdas.
Todo eso suena increíble. Y parte de eso es increíble. Pero hay detalles que me molestan.
Las tres decisiones que me parecen incorrectas
1. El boundary de FFI está en el lugar equivocado
Cuando mezclás Rust con otro runtime, tenés que decidir dónde vive la frontera entre los dos mundos. Ellos eligieron exponer la interfaz en el nivel de string serialization — es decir, los datos cruzan la barrera como JSON strings que después se deserializan del lado de Rust.
Eso es un problema. JSON parsing no es gratis. En operaciones de alta frecuencia, estás pagando el costo de serialización/deserialización en cada llamada. Es el equivalente a tener un sistema de caché brillante y después envolverlo en una capa de compresión innecesaria para transferirlo.
// Lo que hace el boundary en su implementación (aproximación)
const resultado = await rustRuntime.execute(
JSON.stringify(payload) // ← acá está el problema
);
const parsed = JSON.parse(resultado); // ← y acá también
// Lo que debería hacer: typed binary protocol
// MessagePack, FlatBuffers, o directamente shared memory
// para evitar la serialización en el hot path
La alternativa correcta, en mi opinión, es usar un protocolo binario tipado — MessagePack o FlatBuffers — o directamente trabajar con shared memory para el hot path. El overhead de JSON en un runtime de alto rendimiento es exactamente el tipo de fricción que estás intentando eliminar.
2. El modelo de threading asume un patron de uso que no es universal
Rust tiene un modelo de concurrencia que es genuinamente superior para muchos casos. Pero el runtime de TypeScript tiene un event loop de un solo hilo por diseño. La decisión del equipo fue usar un thread pool de Rust para manejar operaciones paralelas, con la lógica de coordinación del lado de TypeScript.
El problema: eso invierte la jerarquía de control. TypeScript termina siendo el orchestrator de un sistema que debería ser coordinado desde Rust. Es como si en tu arquitectura de microservicios pusieras al cliente HTTP como el servicio que decide el routing — técnicamente funciona, pero la responsabilidad está en el lugar equivocado.
Para workloads de CPU-bound esto no importa mucho. Para workloads de I/O-bound con alta concurrencia — que es exactamente donde TypeScript brilla hoy — el overhead de coordinación puede comerse las ganancias.
3. El hot reload está roto por diseño
Esto es más pragmático que arquitectónico, pero me parece importante: el ciclo de desarrollo con este runtime es notablemente más lento. Cada vez que cambiás código TypeScript que toca el boundary de Rust, necesitás recompilar. En desarrollo local eso puede ser 30-60 segundos de espera.
Sé que en producción eso no importa. Pero el tiempo de desarrollo sí importa. Si un runtime de "alto rendimiento" hace que tus devs sean 30% menos productivos durante el desarrollo, el trade-off no es tan claro como parece en los benchmarks.
Es el mismo problema que veo cuando hablo de adopción de nuevos lenguajes — el rendimiento técnico no vive en el vacío. Vive en un equipo, con workflows, con ciclos de feedback.
La decisión que es un genuino golpe de genio
Ahora sí: lo que me parece brillante.
El equipo decidió que Rust no va a tocar el modelo de objetos de TypeScript. Nunca. La capa de Rust es completamente opaca al sistema de tipos de TS — no sabe nada sobre clases, interfaces, genéricos. Solo habla de buffers y operaciones.
Eso parece una limitación. En realidad es una fortaleza enorme.
Significa que el runtime de Rust puede actualizarse independientemente del ecosistema de TypeScript. Cuando TypeScript 6 salga con cambios en el sistema de tipos (y van a salir), el runtime de Rust no necesita actualizarse. La barrera de abstracción está tan limpia que los dos sistemas pueden evolucionar de forma independiente.
// El runtime de Rust no sabe nada de esto:
interface Usuario<T extends Identifiable> {
datos: T;
metadata: Record<string, unknown>;
}
// Solo ve esto:
// [u8; N] — un buffer de bytes con un tamaño
// Eso es todo. Sin tipos. Sin objetos. Sin herencia.
pub fn procesar_buffer(input: &[u8]) -> Vec<u8> {
// lógica de bajo nivel completamente agnóstica al dominio
// sin acoplamientos al sistema de tipos de TypeScript
input.iter().map(|&b| b.wrapping_add(1)).collect()
}
Esa decisión de diseño — mantener la capa de Rust completamente agnóstica al dominio — es exactamente el tipo de cosa que separa un sistema bien diseñado de uno que va a ser un dolor de cabeza en 3 años.
Me recuerda a lo que hacemos con Docker: el container no sabe nada de tu aplicación. Solo sabe de procesos, de redes, de volúmenes. Si te interesa profundizar en esa filosofía de abstracción, hay recursos curados de Docker for novices que trabajan exactamente esa idea.
Los benchmarks que no te muestran
Cada vez que veo benchmarks de rendimiento, busco lo que no está en el gráfico. En este caso:
No muestran el percentil 99. Muestran p50 y p95. El p99 — donde viven los usuarios con peor experiencia — no aparece. En sistemas con garbage collection intermitente (como el runtime de JS que están reemplazando), el p99 puede ser 10x el p95. Si el runtime de Rust mejora el p99 tanto como mejora el p50, ese es el número que debería estar en el título.
No muestran el impacto en errores de memoria. Rust elimina una clase entera de bugs — use-after-free, double-free, data races. Eso tiene valor en producción que no aparece en benchmarks de latencia. Es el mismo tipo de beneficio invisible que aparece en sistemas como los que describí cuando construí el visualizador de colectivos en tiempo real — la parte interesante no es siempre la que se puede medir fácilmente.
No muestran el costo de onboarding. ¿Cuántos devs de TypeScript en tu equipo pueden debuggear un problema en la capa de Rust? Probablemente ninguno, o muy pocos. Eso no aparece en ningún benchmark, pero es real.
Por qué igual me parece interesante
A pesar de todo lo que dije, el experimento me parece valioso. No por los números — por la pregunta que plantea.
¿Cuánto del rendimiento que perdemos en sistemas TypeScript es inherente al lenguaje, y cuánto es de la implementación del runtime? Esa pregunta tiene implicancias enormes para cómo diseñamos sistemas.
Si el cuello de botella es el modelo de memoria del runtime, Rust puede ayudar. Si el cuello de botella es el diseño de tu API, tus queries sin índices, tu arquitectura de caché — Rust no va a cambiar nada. Y eso es algo que aprendí de la forma más concreta posible en 2022.
En ese sentido, me conecta con lo que veo en proyectos de AI on-device como los que está trabajando Apple con sus modelos locales — a veces el constraint de rendimiento te fuerza a tomar decisiones de diseño que resultan ser correctas por razones completamente diferentes a las que esperabas.
Y también me recuerda a proyectos como el sonificador de datos de transporte que construí: cuando tenés un constraint real de performance (procesar miles de eventos GTFS-RT en tiempo real), los trade-offs se vuelven concretos muy rápido. La teoría se evapora.
FAQ: Rust runtime TypeScript rendimiento
¿Necesito aprender Rust para usar un runtime de Rust para TypeScript?
No para usarlo, sí para debuggearlo. Esta es la trampa más común: adoptás la tecnología en producción y cuando algo falla en la capa de Rust, tu equipo no tiene las herramientas para diagnosticarlo. Si vas a adoptar esto, necesitás al menos una persona con conocimiento de Rust capaz de leer stack traces y entender el modelo de memoria.
¿En qué casos tiene sentido el Rust runtime TypeScript para rendimiento real?
Casos donde el cuello de botella es CPU-bound con operaciones de bajo nivel repetitivas: parsing de protocolos, encoding/decoding de datos binarios, criptografía, compresión. Para APIs REST típicas con base de datos, el cuello de botella casi siempre está en las queries o en el I/O de red — ahí Rust no te va a ayudar casi nada.
¿Qué diferencia hay con Deno o Bun que también tienen componentes de alto rendimiento?
Deno usa Rust internamente pero el modelo de programación es completamente TypeScript — no exponés la capa de Rust al desarrollador. Bun usa Zig. Lo que hace el proyecto del post es diferente: crea un boundary explícito entre TypeScript y Rust que el desarrollador tiene que manejar. Más control, más complejidad.
¿El overhead de FFI (Foreign Function Interface) entre TypeScript y Rust cancela las ganancias?
Depende de la granularidad de las llamadas. Si cruzás el boundary una vez por request con un payload grande, el overhead es negligible. Si cruzás el boundary miles de veces por request con payloads pequeños, puede ser peor que no tener Rust. El diseño del boundary es probablemente la decisión más crítica de toda la arquitectura.
¿Esto es comparable a WASM para TypeScript?
WebAssembly es conceptualmente similar pero con restricciones diferentes. WASM puede correr en el browser y en el servidor, tiene un modelo de seguridad sandboxed, y tiene mejor soporte tooling hoy. Rust-to-native tiene menos overhead y más acceso al sistema operativo. Para serverless y edge computing, WASM probablemente gana en simplicidad operacional.
¿Vale la pena el salto si ya tengo TypeScript bien optimizado?
Probablemente no, a menos que hayas agotado las optimizaciones estándar: índices de base de datos, caché, lazy loading, worker threads nativos de Node. La mayoría de los sistemas TypeScript que "necesitan Rust" en realidad necesitan un DBA que mire las queries o alguien que lea el profiler con atención. Yo bajé 40 segundos a 80ms sin tocar el lenguaje — solo con metadata.
Conclusión: el runtime es la pregunta equivocada
Después de leer esto línea por línea, mi posición es esta: el experimento de construir un runtime de Rust para TypeScript es técnicamente fascinante y probablemente inadecuado para el 95% de los casos de uso donde la gente lo va a querer aplicar.
La decisión de mantener Rust completamente agnóstico al sistema de tipos de TypeScript es brillante y debería influenciar cómo pensamos sobre los boundaries de abstracción en general. Las decisiones sobre FFI, threading, y developer experience son mejorables y espero que las próximas versiones las trabajen.
Pero más que cualquier cosa: si estás mirando esto pensando "esto va a resolver mis problemas de performance", primero corré un profiler real. Mirá dónde está tu tiempo. En el 90% de los casos, vas a encontrar que el problema no es el runtime — es una query sin índice, una llamada a una API externa sin timeout, un array que estás recorriendo dos veces cuando podrías recorrerlo una.
Rust es una herramienta extraordinaria para problemas específicos. Y como toda herramienta extraordinaria, el peligro más grande no es usarla mal — es usarla en el problema equivocado.
¿Estás trabajando con TypeScript en producción a escala? ¿Dónde encontraste tus cuellos de botella reales? Me interesa saber.
Este artículo fue publicado originalmente en juanchi.dev
Top comments (1)
Lo del boundary FFI con JSON me resuena desde un ángulo diferente al tuyo — vengo del mundo de infraestructura, no de compiladores, pero el problema es el mismo que veo con los sidecars mal diseñados en Docker: pones una capa de alto rendimiento y después la envuelves en serialización innecesaria en cada llamada. El overhead se come exactamente lo que viniste a ganar.
Lo que me parece más valioso de tu análisis es la distinción entre el cuello de botella real y el cuello de botella percibido. En mi experiencia gestionando infraestructura a escala, el 90% de los problemas de rendimiento que "necesitan" una solución compleja tienen solución en una capa más arriba — una query sin índice, un timeout mal configurado, una llamada en serie que podría ser paralela. La gente llega a Rust cuando tendría que haber llegado antes al profiler.
La decisión de mantener Rust agnóstico al sistema de tipos de TypeScript me parece brillante por la misma razón que valoro los buenos límites de abstracción en arquitectura de servicios: el componente que no sabe nada del dominio es el que puede evolucionar sin romper nada. Es exactamente la filosofía de Docker — el container no sabe qué corre dentro, solo sabe de procesos y redes.
Buen artículo — el punto sobre el p99 que no aparece en los benchmarks es el que más me llevo.