Todos los lenguajes de programación terminan en el mismo lugar: código máquina, las instrucciones binarias que entiende el procesador. No existe un procesador que ejecute JavaScript, Python o Rust directamente. La diferencia entre un lenguaje "rápido" y uno "lento" no está en el destino, sino en cómo y cuándo se hace la traducción, y en qué viaja junto con tu código para mantenerlo funcionando.
Este artículo explica las dos estrategias de traducción, JIT y AOT, qué cuesta cada una, y por qué los tipos de un lenguaje terminan importando para el rendimiento.
Tres formas de llegar a código máquina
Piénsalo como una fila de lenguajes que apuntan al mismo lugar:
- C, C++, Rust → código máquina. El compilador traduce todo antes de ejecutar. El binario corre solo, sin casi nada en medio.
- Go, Java, C# → código máquina + un garbage collector. También se compila antes (Go a nativo, Java y C# a un bytecode que luego se compila), pero dentro del programa viaja un recolector de basura que libera memoria mientras corre.
- JavaScript, Python, Ruby → código máquina + garbage collector + intérprete + compilador JIT. El código se traduce mientras el programa ya está corriendo.
Cada pieza que acompaña a tu código es una ayuda: el recolector te libra de liberar memoria a mano, el intérprete te libra de compilar. Pero cada ayuda cuesta memoria y CPU en tiempo de ejecución. Por eso, con el mismo algoritmo, JavaScript consume más que Go, y Go más que Rust. No porque el lenguaje esté mal hecho, sino porque lleva más compañía.
Qué es un intérprete
Un intérprete lee tu código y lo ejecuta paso a paso, sin producir un binario. Para cada instrucción decide en el momento qué hacer. Es la forma más flexible de ejecutar código, y la más lenta, porque cada línea se vuelve a analizar cada vez que se ejecuta.
Los motores modernos de JavaScript no interpretan el texto fuente directamente: primero lo convierten a bytecode, una representación compacta e intermedia, y es ese bytecode lo que se interpreta. Pero sigue siendo interpretación: hay un programa, el intérprete, ejecutando tu programa.
Qué es un compilador JIT
JIT significa just-in-time: justo a tiempo. Un compilador JIT traduce el código a código máquina mientras el programa está corriendo.
El motor empieza interpretando. Mientras lo hace, cuenta: qué funciones se llaman mucho, qué tipos de datos pasan por cada variable. Cuando una función se vuelve "caliente", el JIT la compila a código máquina optimizado para los tipos que ha visto. A partir de ahí, esa función corre a velocidad nativa.
V8, el motor de Node y Chrome, tiene varios niveles: Ignition interpreta, Sparkplug compila rápido sin optimizar, Maglev optimiza algo, TurboFan optimiza a fondo. JavaScriptCore, el motor de Safari y Bun, tiene una escalera parecida.
Es un sistema muy ingenioso, y es lo que hizo que JavaScript pasara de ser un lenguaje de scripts a correr servidores. Pero paga tres costos:
- Arranque lento. El programa empieza interpretando. El código rápido llega después de "calentar". En un servidor que corre semanas no importa; en una función serverless que arranca con cada petición, o en una CLI que se ejecuta y termina, importa mucho.
- Hilos compilando al lado de tu programa. El JIT corre dos o tres hilos que compilan, descartan y recompilan código mientras tu programa trabaja. Esa memoria y esa CPU no son tuyas, y la memoria incluye el código fuente, el bytecode y varias versiones compiladas de la misma función, todas vivas a la vez.
- Chequeos y desoptimización. El JIT compila para los tipos que ha visto, no para los que el lenguaje garantiza. Si una función recibió números mil veces y de pronto recibe un string, el código optimizado ya no sirve: el motor lo tira y vuelve al intérprete. Para detectar eso, el código compilado está lleno de preguntas: "¿esto sigue siendo un número?".
Qué es la compilación AOT
AOT significa ahead-of-time: por adelantado. El compilador traduce todo el programa a código máquina antes de ejecutarlo, y lo que distribuyes es un binario.
Es lo que siempre han hecho C, C++, Rust y Go: compilas en tu máquina o en el servidor de integración, y el resultado corre sin intérprete, sin calentamiento y sin hilos compilando.
La diferencia práctica con el JIT se resume en una frase: el JIT compila lo que ve en ejecución; el AOT compila lo que puede probar antes de ejecutar.
Eso tiene una consecuencia directa: cuanto más sepa el compilador de antemano, mejor código produce. Y lo que más le sirve saber son los tipos.
Por qué los tipos importan para el rendimiento
Imagina esta función:
function sumar(a: number, b: number) {
return a + b;
}
Si el compilador puede garantizar que a y b siempre serán números, emite una sola instrucción de suma. Si no puede garantizarlo, tiene que emitir código que primero pregunta qué son a y b, y luego decide si suma, concatena strings o llama a un método.
En JavaScript puro, el compilador nunca tiene garantías; solo tiene lo que ha observado. Por eso el JIT llena el código de chequeos.
En TypeScript los tipos existen, pero tradicionalmente se borraban al compilar: TypeScript se convierte en JavaScript, y el motor ve JavaScript. Los tipos eran documentación y autocompletado, nada más.
Lo que cambia con un compilador AOT que lea TypeScript es que, por primera vez, esos tipos llegan al compilador que genera código máquina. Donde puede probar que algo es un número, emite la instrucción directa. Donde no puede, porque hay un any, un cast con as o un dato que entra sin validar desde un JSON.parse, deja un chequeo y un camino lento, como haría el JIT, pero decidido de antemano.
La consecuencia es que el tipado estricto deja de ser documentación y pasa a ser optimización. Un proyecto con strict activado y sin any compila a código más rápido que uno lleno de agujeros de tipo. Como en Go, donde el tipo de cada variable determina cuántos bytes ocupa y qué instrucción la procesa.
Un ejemplo de por qué el compilador no puede confiar ciegamente en los tipos:
function sumar(x: number, y: number) {
return x + y;
}
const datos = JSON.parse('{"x": "5", "y": 3}');
sumar(datos.x, datos.y); // "53", no 8
JSON.parse devuelve any, así que TypeScript no se queja. Pero x llegó como string. Si el compilador hubiera generado una suma de enteros confiando en el tipo declarado, operaría sobre un string y produciría basura o un fallo. Por eso donde el tipo no se puede probar, el chequeo se queda.
Un ejemplo actual: Bun AOT
En octubre de 2026, Jarred Sumner, creador de Bun, publicó un compilador AOT para TypeScript integrado en Bun:
bun build --compile --aot app.ts
Bun parsea el TypeScript, usa los tipos, genera una representación intermedia tipada, optimiza y emite código máquina ARM64 o x64 usando B3, el mismo compilador de bajo nivel que usan los JITs de JavaScriptCore. El resultado es un binario que corre sin intérprete ni JIT.
Los números que mostró sobre código existente, sin cambiar una línea:
- Claude Code CLI: arranque 24% más rápido, 48% menos memoria, 46% menos CPU.
- El compilador de TypeScript 6 (370,000 líneas): 3.3 veces más rápido, 2.7 veces menos memoria.
- Con
--trim, que elimina las partes del runtime que el programa no usa, un servidor HTTP pasó de 89 MB a 25 MB.
En la fila de lenguajes del principio, JavaScript se mueve un puesto: pierde al intérprete y al JIT, y se queda solo con el garbage collector. Pasa a la fila de Go.
Lo que AOT no cambia
Compilar antes de ejecutar elimina al intérprete y al JIT, pero el lenguaje sigue decidiendo qué hace ese código máquina. Hay tres cosas que ningún compilador puede cambiar sin cambiar el lenguaje:
1. El garbage collector. En Rust, el compilador sabe exactamente cuándo termina la vida de cada valor y escribe la liberación en ese punto; nadie corre al lado. En Go, Java o TypeScript con AOT, el recolector viaja dentro del binario y sigue pausando, escaneando y liberando objetos en ejecución.
2. El modelo de datos. En Rust, un struct de tres enteros son 12 bytes contiguos, y un array de mil structs es un bloque de 12,000 bytes en fila. En JavaScript, cada objeto es una asignación independiente en el heap con su cabecera, y un array de objetos es un array de punteros a cosas dispersas por la memoria. Recorrerlo es dar un salto por cada elemento, y eso castiga la caché del procesador. El compilador puede optimizar la lectura de un campo dentro del objeto, pero no puede juntar los objetos: el lenguaje dice que cada {} tiene identidad propia.
3. La precisión numérica. En JavaScript, number es siempre un double de 64 bits. No existe u8, i32 ni f32. Un flag que en Rust ocupa 1 byte, en TypeScript ocupa 8. Y los enteros solo son exactos hasta 2 elevado a 53.
Sobre esto último, JavaScript sí tiene algo con enteros de tamaño fijo desde 2015: los typed arrays, como Int32Array o Float32Array. Nacieron para WebGL, porque la GPU necesita bloques de bytes con formato exacto, y los heredó WebAssembly. Pero son memoria tipada, no variables tipadas: un Int32Array guarda enteros de 32 bits en memoria contigua, pero al leer arr[0] lo que sale es un number normal. Y para usarlos pierdes los objetos: en lugar de puntos[i].x escribes datos[i * 2] y llevas los índices a mano. Es exactamente lo que hacen librerías como Three.js, que guardan posiciones en Float32Array e índices en Uint32Array para pasárselos a la GPU de un solo golpe.
Cada vez que alguien intentó añadir tipos con tamaño que cambien la semántica de JavaScript, terminó con otro lenguaje: AssemblyScript (sintaxis de TypeScript con i32 y u8, compila a WebAssembly, no corre npm), asm.js (anotaciones como x | 0 para marcar enteros), Static TypeScript (TypeScript a C++ para microcontroladores). El propio Sumner lo dijo tras publicar Bun AOT: para que TypeScript alcance el rendimiento de C o Rust, y no solo el de Go, haría falta una sintaxis nueva.
Resumen
- Todos los lenguajes terminan en código máquina. Lo que cambia es cuándo se traduce y qué viaja con tu código.
- Un intérprete ejecuta paso a paso sin producir binario. Flexible y lento.
- Un JIT compila mientras el programa corre, para los tipos que ha visto. Rápido tras calentar, pero paga arranque, hilos extra y chequeos.
- AOT compila antes de ejecutar, para los tipos que puede probar. Sin intérprete, sin calentamiento, sin hilos compilando.
- Con AOT, los tipos dejan de ser documentación y pasan a ser optimización. Cuanto más estricto el tipado, más código directo.
- Lo que AOT no cambia es lo que el lenguaje promete: si hay garbage collector, cómo se guardan los objetos y cuánto mide un número. Para eso hace falta otro lenguaje, o sintaxis nueva.
Durante años, el techo de rendimiento de JavaScript fue el JIT. Con AOT ese techo desaparece, y el siguiente ya es el lenguaje mismo.