Regresar
704

Runtimes de JavaScript: Node vs Deno vs Bun

Actualizado: 15/09/2026

Durante más de una década, ejecutar JavaScript fuera del navegador significaba usar Node, y no había nada que discutir. Hoy hay tres opciones serias, y cada vez que aparece una comparación nueva de rendimiento, alguien se pregunta si debería cambiar.

La respuesta casi nunca depende de lo que miden esas comparaciones. Depende de algo mucho más aburrido: si todo lo que tu proyecto ya usa funciona en el runtime nuevo.

Qué es exactamente un runtime

JavaScript es un lenguaje; por sí solo no sabe leer archivos, abrir conexiones de red ni acceder a variables de entorno. Esas capacidades las aporta el entorno que lo ejecuta.

Un runtime es ese entorno: un motor que interpreta el código, más un conjunto de funciones para hablar con el sistema operativo, más las herramientas que lo acompañan. Dos runtimes pueden ejecutar el mismo lenguaje y ofrecer funciones distintas, con nombres distintos, para lo mismo.

Ahí está el origen de todas las incompatibilidades: el código que solo usa el lenguaje funciona en cualquiera, y el que usa funciones propias de un entorno solo funciona en ese entorno.

Lo mismo por dentro, distinto alrededor

Los tres runtimes ejecutan el mismo JavaScript y TypeScript. Node es el más compatible, con todo el ecosistema npm y base de casi todo lo existente. Deno ofrece permisos explícitos, TypeScript sin configuración y compatibilidad con npm. Bun tiene arranque muy rápido e incluye instalador y pruebas, con compatibilidad aún incompleta
La pregunta útil no es cuál es más rápido, es si tus dependencias funcionan en él.

Los tres han convergido bastante en los últimos años. Deno y Bun añadieron compatibilidad con los paquetes de npm, que era la barrera principal, y Node incorporó varias de las comodidades que los otros ofrecían, como ejecutar TypeScript directamente o un ejecutor de pruebas propio.

Esa convergencia hace que la diferencia entre ellos sea hoy más de enfoque y de madurez que de capacidades.

Node: la opción que no hay que justificar

Node es donde está casi todo. Las librerías se prueban primero en Node, los proveedores de alojamiento lo soportan sin configuración, y cualquier error que encuentres ya lo encontró alguien antes y está resuelto en algún foro.

Esa madurez tiene un valor que no aparece en ninguna medición de rendimiento: la probabilidad de encontrarte con un problema que nadie ha visto antes es mínima.

Sus puntos débiles son históricos. Carga con decisiones de diseño antiguas, su modelo de módulos convivió durante años con dos sistemas incompatibles, y ejecutar código no confiable exige herramientas externas porque por defecto cualquier script tiene acceso completo al sistema.

Para un proyecto que empieza hoy sin requisitos especiales, sigue siendo la elección por defecto.

Deno: seguridad por defecto

La diferencia más importante de Deno no es la velocidad ni la comodidad, es su modelo de permisos: un script no puede leer archivos, acceder a la red ni leer variables de entorno salvo que se le autorice explícitamente al ejecutarlo.

# Sin permisos: el script falla si intenta acceder a la red o al disco
deno run script.ts

# Permisos concretos: solo esta dirección y solo lectura de esta carpeta
deno run --allow-net=api.ejemplo.com --allow-read=./datos script.ts

Ese modelo tiene una consecuencia directa sobre un riesgo que tratamos en el eje de seguridad: una dependencia comprometida no puede enviar tus credenciales a ningún sitio si el proceso no tiene permiso de red hacia ese destino.

A cambio, hay que declarar los permisos, lo que añade fricción, y algunas librerías pensadas para Node esperan acceso libre y fallan de formas poco claras.

Bun: velocidad y todo incluido

Bun apuesta por dos cosas: rendimiento, especialmente en el arranque y en la instalación de dependencias, y reunir en un solo binario lo que en Node requiere varias herramientas: el instalador de paquetes, el ejecutor de pruebas y el empaquetador.

# Instalación de dependencias, normalmente mucho más rápida
bun install

# Ejecutar TypeScript directamente, sin paso de compilación
bun run servidor.ts

# Ejecutor de pruebas incluido, con una sintaxis compatible con la habitual
bun test

La velocidad de instalación se nota de verdad en el día a día y en los procesos de integración, donde se instalan dependencias en cada ejecución.

Su punto débil es la compatibilidad. Aunque soporta la gran mayoría de lo que hace Node, los casos que no soporta aparecen justo en proyectos grandes con dependencias nativas o poco comunes, y descubrirlos a mitad de una migración es caro.

Los tres, comparados

NodeDenoBun
Compatibilidad con npmTotalAltaAlta, con huecos
Acceso al sistema por defectoCompletoNingunoCompleto
TypeScript sin configurarReciente, básicoSíSí
Instalador y pruebas incluidosParcialSíSí
Soporte en alojamientosUniversalAmplioCreciendo
Madurez y problemas ya resueltosMáximaAltaMedia
Dónde encajaCasi todoScripts y servicios nuevosHerramientas y proyectos nuevos

La fila del acceso al sistema es la que más consecuencias tiene y la que menos aparece en las comparaciones. Es la única diferencia de las tres que cambia la superficie de riesgo del proyecto, no solo la experiencia de programar.

Comprobar la compatibilidad antes de decidir

La comparación que importa no está en ningún artículo: es la de tu propio proyecto. Y hacerla lleva menos de una hora.

# 1. Qué dependencias usan código nativo (las que más fallan al cambiar)
find node_modules -name "*.node" | cut -d/ -f2 | sort -u

# 2. Ejecutar la batería de pruebas existente con cada runtime
npm test
bun test
deno test --allow-all

# 3. Arrancar el servidor y lanzar una carga corta para comparar en tu caso real
bun run servidor.ts &
npx autocannon -c 50 -d 15 http://localhost:3000/api/pedidos

El primer comando es el que más información da en menos tiempo: las dependencias con componentes nativos son las candidatas a romperse, y si no hay ninguna, la migración suele ser sencilla.

Y el tercero pone en su sitio las comparaciones de rendimiento publicadas: en una aplicación que pasa la mayor parte del tiempo esperando a la base de datos, la diferencia entre runtimes se reduce mucho, porque el runtime no hace más rápida la consulta.

Cuándo tiene sentido cambiar

Para un proyecto existente en Node que funciona, el cambio casi nunca compensa. El ahorro de rendimiento suele ser pequeño en aplicaciones reales, y el riesgo de encontrar una incompatibilidad en producción es real.

Donde sí tiene sentido es en piezas nuevas y acotadas: un script de automatización, una herramienta interna, un servicio pequeño sin dependencias complejas. Ahí se aprovechan las ventajas sin arriesgar el sistema principal.

Hay un caso intermedio útil y de bajo riesgo: usar Bun solo como instalador de paquetes y ejecutor de pruebas, manteniendo Node para ejecutar la aplicación. Se gana velocidad en el trabajo diario y en la integración continua sin tocar lo que corre en producción.

Y hay un caso claro para Deno: cuando se ejecuta código de terceros o de origen dudoso, su modelo de permisos aporta una protección que en los otros dos requiere montar contenedores o herramientas adicionales.

Fijar la versión, que importa más que elegir

Hay una decisión que tiene más impacto en la estabilidad diaria que la elección de runtime, y que se deja sin tomar con mucha frecuencia: qué versión exacta se usa en cada sitio.

El escenario típico es conocido. El proyecto funciona en la máquina de quien lo programó con una versión, el servidor de integración usa otra y producción una tercera. Un día se actualiza una de ellas y aparece un error que nadie puede reproducir, porque en cada máquina el código se comporta distinto.

La solución es declarar la versión en el propio repositorio, de forma que todas las herramientas la lean del mismo sitio.

# Declarar la versión en el repositorio (leída por nvm, fnm y muchos alojamientos)
echo "22.11.0" > .nvmrc

# Y en package.json, para que la instalación avise si no coincide
npm pkg set engines.node=">=22.11 <23"

# Comprobar que la versión activa coincide antes de ejecutar nada
node --version
nvm use   # o: fnm use

Conviene además que el proceso de integración falle si la versión no coincide, en lugar de avisar y continuar. Un aviso que se ignora es equivalente a no tener nada.

Y hay un detalle sobre las versiones de soporte extendido que ahorra sustos: Node publica versiones pares con soporte de varios años y versiones impares de vida corta. Para producción conviene usar siempre una par en fase de soporte, y conocer su fecha de fin, porque dejar de recibir parches de seguridad es un riesgo real que llega sin avisar.

El caso de las funciones en el borde y sin servidor

La elección de runtime a veces no la tomas tú, y conviene saberlo antes de escribir el código.

Las plataformas de funciones en el borde, que vimos en el eje de diseño de API, ejecutan un entorno limitado que no es exactamente ninguno de los tres: comparte con ellos las funciones estándar de la web, pero no incluye buena parte de lo que ofrece Node, como el acceso al sistema de archivos.

La forma de escribir código que funcione en todos esos sitios es apoyarse en las funciones estándar compartidas, como las de peticiones web, flujos de datos y cifrado, y evitar las propias de un runtime concreto. Existe un esfuerzo de estandarización precisamente para definir ese conjunto común.

La regla práctica es identificar al principio dónde se va a ejecutar cada pieza. Una librería que funciona perfectamente en el servidor puede ser inservible en el borde, y descubrirlo después de escribir la integración es trabajo perdido.

Qué se le escapa a una IA con esto

El código generado para Node suele usar funciones propias de Node sin mencionarlo, lo que está bien si el proyecto usa Node y produce errores confusos si se ejecuta en otro runtime. Lo contrario también ocurre: código escrito con funciones propias de Bun o Deno que falla al desplegar en un entorno con Node.

Hay además una tendencia a recomendar cambiar de runtime citando mejoras de rendimiento de pruebas sintéticas, sin preguntar qué hace la aplicación ni qué dependencias tiene.

Las frases que cambian la respuesta son indicar explícitamente en qué runtime y versión se va a ejecutar el código, y pedir que se usen solo las funciones estándar del lenguaje cuando se quiera que funcione en cualquiera de los tres.

Y ante una propuesta de migración, conviene pedir la lista de dependencias con componentes nativos antes que cualquier comparación de velocidad. Esa lista decide la viabilidad; la velocidad es un detalle.


Comparativa intermedio