El caché de bytecode es una optimización en tiempo de compilación que mejora drásticamente el tiempo de inicio de la aplicación al precompilar tu JavaScript a bytecode. Por ejemplo, al compilar tsc de TypeScript con bytecode habilitado, el tiempo de inicio mejora 2 veces.
Uso
Uso básico
Habilita el caché de bytecode con la bandera --bytecode:
bun build ./index.ts --target=bun --bytecode --outdir=./distEsto genera dos archivos:
dist/index.js- Tu JavaScript empaquetadodist/index.jsc- El archivo de caché de bytecode
En tiempo de ejecución, Bun detecta y usa automáticamente el archivo .jsc:
bun ./dist/index.js # Usa automáticamente index.jscCon ejecutables independientes
Al crear ejecutables con --compile, el bytecode se incrusta en el binario:
bun build ./cli.ts --compile --bytecode --outfile=mycliEl ejecutable resultante contiene tanto el código como el bytecode, ofreciéndote el máximo rendimiento en un solo archivo.
Combinando con otras optimizaciones
El bytecode funciona muy bien con minificación y mapas de origen:
bun build --compile --bytecode --minify --sourcemap ./cli.ts --outfile=mycli--minifyreduce el tamaño del código antes de generar bytecode (menos código -> menos bytecode)--sourcemappreserva el reporte de errores (los errores aún apuntan al origen original)--bytecodeelimina la sobrecarga de análisis
Impacto en el rendimiento
La mejora de rendimiento escala con el tamaño de tu base de código:
| Tamaño de la aplicación | Mejora típica de inicio |
|---|---|
| CLI pequeña (< 100 KB) | 1.5-2 veces más rápido |
| App mediana-grande (> 5 MB) | 2.5-4 veces más rápido |
Las aplicaciones más grandes se benefician más porque tienen más código que analizar.
Cuándo usar bytecode
Ideal para:
Herramientas CLI
- Invocadas frecuentemente (linters, formatters, ganchos git)
- El tiempo de inicio es toda la experiencia del usuario
- Los usuarios notan la diferencia entre 90ms y 45ms de inicio
- Ejemplo: compilador de TypeScript, Prettier, ESLint
Herramientas de construcción y ejecutores de tareas
- Se ejecutan cientos o miles de veces durante el desarrollo
- Los milisegundos ahorrados por ejecución se acumulan rápidamente
- Mejora de la experiencia del desarrollador
- Ejemplo: scripts de construcción, ejecutores de pruebas, generadores de código
Ejecutables independientes
- Distribuidos a usuarios que valoran el rendimiento ágil
- La distribución en un solo archivo es conveniente
- El tamaño del archivo es menos importante que el tiempo de inicio
- Ejemplo: CLIs distribuidos vía npm o como binarios
Evítalo para:
- ❌ Scripts pequeños
- ❌ Código que se ejecuta una vez
- ❌ Compilaciones de desarrollo
- ❌ Entornos con restricciones de tamaño
- ❌ Código con await de nivel superior (no soportado)
Limitaciones
Solo CommonJS
El caché de bytecode actualmente funciona con el formato de salida CommonJS. El empaquetador de Bun convierte automáticamente la mayoría del código ESM a CommonJS, pero await de nivel superior es la excepción:
// Esto previene el caché de bytecode
const data = await fetch("https://api.example.com");
export default data;Por qué: El await de nivel superior requiere evaluación asíncrona del módulo, lo cual no puede representarse en CommonJS. El grafo del módulo se vuelve asíncrono y el modelo de función envoltorio CommonJS se rompe.
Solución: Mueve la inicialización asíncrona dentro de una función:
async function init() {
const data = await fetch("https://api.example.com");
return data;
}
export default init;Ahora el módulo exporta una función que el consumidor puede esperar cuando sea necesario.
Compatibilidad de versiones
El bytecode no es portable entre versiones de Bun. El formato de bytecode está ligado a la representación interna de JavaScriptCore, que cambia entre versiones.
Cuando actualices Bun, debes regenerar el bytecode:
# Después de actualizar Bun
bun build --bytecode ./index.ts --outdir=./distSi el bytecode no coincide con la versión actual de Bun, se ignora automáticamente y tu código vuelve a analizar el origen JavaScript. Tu aplicación sigue funcionando - solo pierdes la optimización de rendimiento.
Mejor práctica: Genera bytecode como parte de tu proceso de construcción CI/CD. No hagas commit de archivos .jsc a git. Regenéralos cada vez que actualices Bun.
El código fuente sigue siendo necesario
- El archivo
.js(tu código fuente empaquetado) - El archivo
.jsc(el caché de bytecode)
En tiempo de ejecución:
- Bun carga el archivo
.js, ve un pragma@bytecodey verifica el archivo.jsc - Bun carga el archivo
.jsc - Bun valida que el hash del bytecode coincida con el origen
- Si es válido, Bun usa el bytecode
- Si es inválido, Bun vuelve a analizar el origen
El bytecode no es ofuscación
El bytecode no oscurece tu código fuente. Es una optimización, no una medida de seguridad.
Despliegue en producción
Docker
Incluye la generación de bytecode en tu Dockerfile:
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build --bytecode --minify --sourcemap \
--target=bun \
--outdir=./dist \
--compile \
./src/server.ts --outfile=./dist/server
FROM oven/bun:1 AS runner
WORKDIR /app
COPY --from=builder /dist/server /app/server
CMD ["./server"]El bytecode es independiente de la arquitectura.
CI/CD
Genera bytecode durante tu pipeline de construcción:
# GitHub Actions
- name: Construir con bytecode
run: |
bun install
bun build --bytecode --minify \
--outdir=./dist \
--target=bun \
./src/index.tsDepuración
Verificar que el bytecode está siendo usado
Verifica que el archivo .jsc existe:
ls -lh dist/-rw-r--r-- 1 user staff 245K index.js
-rw-r--r-- 1 user staff 1.1M index.jscEl archivo .jsc debería ser 2-8 veces más grande que el archivo .js.
Para registrar si el bytecode está siendo usado, establece BUN_JSC_verboseDiskCache=1 en tu entorno.
Con éxito, registrará algo como:
[Disk cache] cache hit for sourceCodeSi ves un fallo de caché, registrará algo como:
[Disk cache] cache miss for sourceCodeEs normal que registre un fallo de caché múltiples veces ya que Bun actualmente no almacena en caché de bytecode el código JavaScript usado en módulos integrados.
Problemas comunes
Bytecode ignorado silenciosamente: Generalmente causado por una actualización de versión de Bun. La versión del caché no coincide, así que el bytecode es rechazado. Regenera para solucionar.
Tamaño de archivo demasiado grande: Esto es esperado. Considera:
- Usar
--minifypara reducir el tamaño del código antes de generar bytecode - Comprimir archivos
.jscpara transferencia de red (gzip/brotli) - Evaluar si la ganancia de rendimiento de inicio vale el aumento de tamaño
Await de nivel superior: No soportado. Refactoriza para usar funciones de inicialización asíncronas.
¿Qué es el bytecode?
Cuando ejecutas JavaScript, el motor JavaScript no ejecuta tu código fuente directamente. En cambio, pasa por varios pasos:
- Análisis: El motor lee tu código fuente JavaScript y lo convierte en un Árbol de Sintaxis Abstracta (AST)
- Compilación a bytecode: El AST se compila a bytecode - una representación de nivel inferior que es más rápida de ejecutar
- Ejecución: El bytecode es ejecutado por el intérprete del motor o compilador JIT
El bytecode es una representación intermedia - es de nivel inferior que el código fuente JavaScript, pero de nivel superior que el código máquina. Piénsalo como lenguaje ensamblador para una máquina virtual. Cada instrucción de bytecode representa una operación única como "cargar esta variable", "sumar dos números" o "llamar a esta función".
Esto sucede cada vez que ejecutas tu código. Si tienes una herramienta CLI que se ejecuta 100 veces al día, tu código se analiza 100 veces. Si tienes una función serverless con inicios en frío frecuentes, el análisis sucede en cada inicio en frío.
Con el caché de bytecode, Bun mueve los pasos 1 y 2 al paso de construcción. En tiempo de ejecución, el motor carga el bytecode precompilado y salta directamente a la ejecución.
Por qué el análisis diferido hace esto aún mejor
Los motores JavaScript modernos usan una optimización inteligente llamada análisis diferido. No analizan todo tu código de inmediato - en cambio, las funciones solo se analizan cuando se llaman por primera vez:
// Sin caché de bytecode:
function rarely_used() {
// Esta función de 500 líneas solo se analiza
// cuando realmente se llama
}
function main() {
console.log("Iniciando app");
// rarely_used() nunca se llama, así que nunca se analiza
}Esto significa que la sobrecarga de análisis no es solo un costo de inicio - sucede durante toda la vida de tu aplicación a medida que se ejecutan diferentes rutas de código. Con el caché de bytecode, todas las funciones están precompiladas, incluso las que se analizan diferidamente. El trabajo de análisis sucede una vez en tiempo de construcción en lugar de distribuirse durante la ejecución de tu aplicación.
El formato de bytecode
Dentro de un archivo .jsc
Un archivo .jsc contiene una estructura de bytecode serializada. Entender qué hay dentro ayuda a explicar tanto los beneficios de rendimiento como la compensación del tamaño del archivo.
Sección de cabecera (validada en cada carga):
- Versión del caché: Un hash ligado a la versión del framework JavaScriptCore. Esto asegura que el bytecode generado con una versión de Bun solo se ejecute con esa versión exacta.
- Etiqueta de tipo de bloque de código: Identifica si es un bloque de código Programa, Módulo, Eval o Función.
SourceCodeKey (valida que el bytecode coincide con el origen):
- Hash del código fuente: Un hash del código fuente JavaScript original. Bun verifica que coincida antes de usar el bytecode.
- Longitud del código fuente: La longitud exacta del origen, para validación adicional.
- Banderas de compilación: Contexto de compilación crítico como modo estricto, si es un script vs módulo, tipo de contexto eval, etc. El mismo código fuente compilado con diferentes banderas produce diferente bytecode.
Instrucciones de bytecode:
- Flujo de instrucciones: Los opcodes de bytecode reales - la representación compilada de tu JavaScript. Esta es una secuencia de longitud variable de instrucciones de bytecode.
- Tabla de metadatos: Cada opcode tiene metadatos asociados - cosas como contadores de perfilado, sugerencias de tipo y conteos de ejecución (incluso si aún no están poblados).
- Objetivos de salto: Direcciones precomputadas para flujo de control (if/else, bucles, declaraciones switch).
- Tablas switch: Tablas de búsqueda optimizadas para declaraciones switch.
Constantes e identificadores:
- Pool de constantes: Todos los valores literales en tu código - números, cadenas, booleanos, null, undefined. Estos se almacenan como valores JavaScript reales (JSValues) así que no necesitan ser analizados desde el origen en tiempo de ejecución.
- Tabla de identificadores: Todos los nombres de variables y funciones usados en el código. Almacenados como cadenas deduplicadas.
- Marcadores de representación del código fuente: Banderas que indican cómo deben representarse las constantes (como enteros, dobles, big ints, etc.).
Metadatos de función (para cada función en tu código):
- Asignación de registros: Cuántos registros (variables locales) necesita la función -
thisRegister,scopeRegister,numVars,numCalleeLocals,numParameters. - Características de código: Una máscara de bits de características de función: ¿es un constructor? ¿una función flecha? ¿usa
super? ¿tiene llamadas de cola? Estas afectan cómo se ejecuta la función. - Características de ámbito léxico: Modo estricto y otro contexto léxico.
- Modo de análisis: El modo en que se analizó la función (normal, async, generador, generador async).
Estructuras anidadas:
- Declaraciones y expresiones de función: Cada función anidada obtiene su propio bloque de bytecode, recursivamente. Un archivo con 100 funciones tiene 100 bloques de bytecode separados, todos anidados en la estructura.
- Manejadores de excepciones: Bloques try/catch/finally con sus límites y direcciones de manejador precomputados.
- Información de expresión: Mapea posiciones de bytecode de vuelta a ubicaciones de código fuente para reporte de errores y depuración.
Qué NO contiene el bytecode
Importantemente, el bytecode no incrusta tu código fuente. En cambio:
- El origen JavaScript se almacena por separado (en el archivo
.js) - El bytecode solo almacena un hash y longitud del origen
- En tiempo de carga, Bun valida que el bytecode coincida con el código fuente actual
Por esto necesitas desplegar tanto el archivo .js como el .jsc. El archivo .jsc es inútil sin su archivo .js correspondiente.
La compensación: tamaño de archivo
Los archivos de bytecode son significativamente más grandes que el código fuente - típicamente 2-8 veces más grandes.
¿Por qué el bytecode es mucho más grande?
Las instrucciones de bytecode son verbosas: Una sola línea de JavaScript minificado puede compilarse a docenas de instrucciones de bytecode. Por ejemplo:
const sum = arr.reduce((a, b) => a + b, 0);Se compila a bytecode que:
- Carga la variable
arr - Obtiene la propiedad
reduce - Crea la función flecha (que ella misma tiene bytecode)
- Carga el valor inicial
0 - Configura la llamada con el número correcto de argumentos
- Realiza realmente la llamada
- Almacena el resultado en
sum
Cada uno de estos pasos es una instrucción de bytecode separada con sus propios metadatos.
Los pools de constantes almacenan todo: Cada literal de cadena, número, nombre de propiedad - todo se almacena en el pool de constantes. Incluso si tu código fuente tiene "hello" cien veces, el pool de constantes lo almacena una vez, pero la tabla de identificadores y las referencias constantes añaden sobrecarga.
Metadatos por función: Cada función - incluso funciones pequeñas de una línea - obtiene sus propios metadatos completos:
- Información de asignación de registros
- Máscara de bits de características de código
- Modo de análisis
- Manejadores de excepciones
- Información de expresión para depuración
Un archivo con 1,000 funciones pequeñas tiene 1,000 conjuntos de metadatos.
Estructuras de datos de perfilado: Aunque los datos de perfilado aún no están poblados, las estructuras para mantener datos de perfilado están asignadas. Esto incluye:
- Ranuras de perfil de valor (rastreo de qué tipos fluyen a través de cada operación)
- Ranuras de perfil de array (rastreo de patrones de acceso a arrays)
- Ranuras de perfil aritmético binario (rastreo de tipos de números en operaciones matemáticas)
- Ranuras de perfil aritmético unario
Estos ocupan espacio incluso cuando están vacíos.
Flujo de control precomputado: Los objetivos de salto, tablas switch y límites de manejadores de excepciones están todos precomputados y almacenados. Esto hace la ejecución más rápida pero aumenta el tamaño del archivo.
Estrategias de mitigación
Compresión: El bytecode se comprime extremadamente bien con gzip/brotli (60-70% de compresión). La estructura repetitiva y metadatos se comprimen eficientemente.
Minificación primero: Usar --minify antes de generar bytecode ayuda:
- Identificadores más cortos → tabla de identificadores más pequeña
- Eliminación de código muerto → menos bytecode generado
- Plegado de constantes → menos constantes en el pool
La compensación: Estás intercambiando archivos 2-4 veces más grandes por inicio 2-4 veces más rápido. Para CLIs, esto usualmente vale la pena. Para servidores de larga ejecución donde unos pocos megabytes de espacio en disco no importan, es aún menos problemático.
Versionado y portabilidad
Portabilidad entre arquitecturas: ✅
El bytecode es independiente de la arquitectura. Puedes:
- Construir en macOS ARM64, desplegar en Linux x64
- Construir en Linux x64, desplegar en AWS Lambda ARM64
- Construir en Windows x64, desplegar en macOS ARM64
El bytecode contiene instrucciones abstractas que funcionan en cualquier arquitectura. Las optimizaciones específicas de arquitectura suceden durante la compilación JIT en tiempo de ejecución, no en el bytecode en caché.
Portabilidad entre versiones: ❌
El bytecode no es estable entre versiones de Bun. Aquí está el porqué:
El formato de bytecode cambia: El formato de bytecode de JavaScriptCore evoluciona. Se añaden nuevos opcodes, se eliminan o cambian los antiguos, las estructuras de metadatos cambian. Cada versión de JavaScriptCore tiene un formato de bytecode diferente.
Validación de versión: La versión del caché en la cabecera del archivo .jsc es un hash del framework JavaScriptCore. Cuando Bun carga bytecode:
- Extrae la versión del caché del archivo
.jsc - Computa la versión actual de JavaScriptCore
- Si no coinciden, el bytecode es silenciosamente rechazado
- Bun vuelve a analizar el código fuente
.js
Tu aplicación sigue funcionando - solo pierdes la optimización de rendimiento.
Degradación elegante: Este diseño significa que el caché de bytecode "falla abierto" - si algo sale mal (incompatibilidad de versión, archivo corrupto, archivo faltante), tu código sigue funcionando normalmente. Puedes ver un inicio más lento, pero no verás errores.
Bytecode no vinculado vs vinculado
JavaScriptCore hace una distinción crucial entre bytecode "no vinculado" y "vinculado". Esta separación es lo que hace posible el caché de bytecode:
Bytecode no vinculado (lo que se almacena en caché)
El bytecode guardado en archivos .jsc es bytecode no vinculado. Contiene:
- Las instrucciones de bytecode compiladas
- Información estructural sobre el código
- Constantes e identificadores
- Información de flujo de control
Pero no contiene:
- Punteros a objetos de tiempo de ejecución reales
- Código máquina compilado JIT
- Datos de perfilado de ejecuciones anteriores
- Información de enlace de llamadas (qué funciones llaman a cuáles)
El bytecode no vinculado es inmutable y compartible. Múltiples ejecuciones del mismo código pueden referenciar el mismo bytecode no vinculado.
Bytecode vinculado (ejecución en tiempo de ejecución)
Cuando Bun ejecuta bytecode, lo "vincula" - creando un envoltorio de tiempo de ejecución que añade:
- Información de enlace de llamadas: A medida que tu código se ejecuta, el motor aprende qué funciones llaman a cuáles y optimiza esos sitios de llamada.
- Datos de perfilado: El motor rastrea cuántas veces se ejecuta cada instrucción, qué tipos de valores fluyen a través del código, patrones de acceso a arrays, etc.
- Estado de compilación JIT: Referencias a versiones compiladas JIT base u optimizante (DFG/FTL) de código caliente.
- Objetos de tiempo de ejecución: Punteros a objetos JavaScript reales, prototipos, ámbitos, etc.
Esta representación vinculada se crea nueva cada vez que ejecutas tu código. Esto permite:
- Almacenar en caché el trabajo costoso (análisis y compilación a bytecode no vinculado)
- Seguir recopilando datos de perfilado en tiempo de ejecución para guiar optimizaciones
- Seguir aplicando optimizaciones JIT basadas en patrones de ejecución reales
El caché de bytecode mueve el trabajo costoso (análisis y compilación a bytecode) de tiempo de ejecución a tiempo de construcción. Para aplicaciones que inician frecuentemente, esto puede reducir a la mitad tu tiempo de inicio al costo de archivos más grandes en disco.
Para CLIs de producción y despliegues serverless, la combinación de --bytecode --minify --sourcemap te da el mejor rendimiento manteniendo la depurabilidad.