{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive","url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive","title":"Internals de Bun v2: JavaScriptCore, Allocators de Zig y Batching de Syscalls con io_uring","description":"Análisis de la arquitectura de Bun v2: los tradeoffs de JavaScriptCore vs V8, el diseño del arena allocator de Zig, batching de syscalls con io_uring, y las decisiones de ingeniería detrás de las mejoras de 3x en cold start.","language":"es","datePublished":"2025-10-04","dateModified":null,"contentFormat":"text/markdown","content":"JavaScript Runtimes\n\n# Internals de Bun v2: JavaScriptCore, Allocators de Zig y Batching de Syscalls con io\\_uring\n\n2025-10-0410 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/bun-v2-runtime-internals-deep-dive>)\n\n## La Apuesta por JavaScriptCore\n\nLa decisión más contrarian de la arquitectura de [Bun](<https://bun.sh/>) es usar JavaScriptCore (JSC) en vez de V8. Todos los otros runtimes de JavaScript, [Node.js](<https://nodejs.org/>), [Deno](<https://deno.com/>), [Cloudflare Workers](<https://workers.cloudflare.com/>), usan V8. Jarred eligió JSC, y en Bun v2, esa apuesta ha rendido más que nunca.\n\nUn aspecto que suele pasarse por alto: V8 y JSC optimizan para perfiles de workload distintos.\n\nPipeline de V8: **Ignition** (intérprete) → **Sparkplug** (baseline rápido) → **Maglev** (mid-tier) → **TurboFan** (compilador optimizador)\n\nPipeline de JSC: **LLInt** (intérprete) → **Baseline JIT** → **DFG** (data flow graph) → **FTL** (faster than light, vía B3/LLVM)\n\nLa diferencia clave no es el rendimiento pico, TurboFan y FTL producen código comparable para hot loops. Es el time-to-optimized-code. El Baseline JIT de JSC arranca después de ~6 invocaciones de una función. Sparkplug de V8 necesita ~100. DFG arranca a ~60 invocaciones; Maglev a ~400.\n\nPara una función serverless que hace cold-start, maneja 10 requests, y se evicciona? JSC está corriendo código máquina optimizado mientras V8 todavía está en el intérprete para la mitad de tus code paths.\n\nUn benchmark con una cadena de middleware realista estilo Express (8 middlewares, parsing JSON, validación, mock de query a DB) revela la diferencia:\n\n```\nCold start + primer request:\n Node.js 22 (V8): 147ms\n Bun v2 (JSC): 48ms\n\nDespués de 100 requests:\n Node.js 22 (V8): 3.2ms/req\n Bun v2 (JSC): 2.8ms/req\n\nDespués de 10,000 requests:\n Node.js 22 (V8): 1.1ms/req\n Bun v2 (JSC): 1.3ms/req\n```\n\nEl crossover es claro. V8 gana en throughput sostenido porque las optimizaciones especulativas de TurboFan son más agresivas. Sin embargo, Bun gana en todos los escenarios donde importa el startup y el warm-up, que es la mayor parte de los despliegues reales.\n\n## El Allocator de Zig: Por Qué Importa\n\nBun está escrito en [Zig](<https://ziglang.org/>), y la elección tiene implicaciones de ingeniería concretas. El modelo de memoria de Zig le da a Bun capacidades que serían extremadamente difíciles en C++ o Rust.\n\nEl ` GeneralPurposeAllocator ` estándar de Zig es adecuado, pero Bun v2 usa un allocator personalizado basado en arenas para el manejo de requests. La observación clave es la siguiente: la mayoría del procesamiento de requests HTTP asigna una gran cantidad de objetos pequeños, los usa por unos milisegundos, y después descarta todo. Un allocator tradicional (malloc/free) tiene que rastrear cada asignación individualmente. Un arena allocator simplemente avanza un puntero, y libera todo de una cuando el request termina.\n\n```zig\n// Versión simplificada del allocator per-request de Bun\nconst RequestArena = struct {\n buffer: [*]u8,\n offset: usize,\n capacity: usize,\n\n pub fn alloc(self: *RequestArena, size: usize, alignment: u8) ?[*]u8 {\n const aligned_offset = std.mem.alignForward(usize, self.offset, alignment);\n if (aligned_offset + size > self.capacity) return null;\n const ptr = self.buffer + aligned_offset;\n self.offset = aligned_offset + size;\n return ptr;\n }\n\n pub fn reset(self: *RequestArena) void {\n // Una operación libera TODO\n self.offset = 0;\n }\n};\n```\n\nEsto es extremadamente rápido. Sin free lists, sin coalescing, sin fragmentación. La llamada a ` reset() ` al final de cada request es literalmente setear un entero a cero. Al perfilar un servidor HTTP de Bun manejando 50k req/s, el overhead del allocator se ubica debajo del 0.3% del tiempo total de CPU. En Node.js, el garbage collector de V8 toma 4-7% en el mismo workload.\n\nEl tradeoff es que los objetos alocados en arena no pueden sobrevivir al request. Bun maneja esto detectando alocaciones que \"escapan\", objetos que se guardan en globals o closures, y promoviéndolos al heap de propósito general. Esta detección pasa a nivel de compilación de Zig usando anotaciones de lifetime que el sistema de tipos de Zig hace naturales.\n\n## io\\_uring: La Ventaja Linux\n\nEn este punto es donde Bun v2 obtiene la mayor ventaja en Linux. ` io_uring ` es una interfaz del kernel que permite agrupar múltiples operaciones de I/O en un solo syscall. En vez de:\n\n```\nsyscall: open(\"config.json\") → fd 5\nsyscall: fstat(5) → {size: 1024}\nsyscall: read(5, buf, 1024) → 1024 bytes\nsyscall: close(5)\nsyscall: socket(AF_INET, ...) → fd 6\nsyscall: connect(6, addr) → 0\nsyscall: write(6, request) → 256 bytes\nsyscall: read(6, response) → 512 bytes\n```\n\nEso son 8 syscalls, cada uno involucrando un context switch user-kernel que cuesta ~1-3 microsegundos. Con io\\_uring, Bun hace:\n\n```\n// Submit queue: batchear todas las operaciones\nsqe[0] = { .op = OPENAT, .path = \"config.json\" }\nsqe[1] = { .op = READ, .fd = CHAIN_PREV, .buf = ... }\nsqe[2] = { .op = SOCKET, .domain = AF_INET, ... }\nsqe[3] = { .op = CONNECT, .fd = CHAIN_PREV, .addr = ... }\nsqe[4] = { .op = SEND, .fd = CHAIN_PREV, .buf = request }\nsqe[5] = { .op = RECV, .fd = CHAIN_PREV, .buf = response }\n\n// Un syscall para submitir todo, uno para cosechar completions\nsyscall: io_uring_enter(ring_fd, 6, 6, IORING_ENTER_GETEVENTS)\n```\n\nDos syscalls en vez de ocho. El flag ` CHAIN_PREV ` es crucial, le dice al kernel que la operación N depende del fd de la operación N-1, así que el kernel las encadena automáticamente. El event loop de Bun v2 está construido alrededor de esto: recolecta todas las operaciones I/O pendientes durante un checkpoint de microtask, las batchea en una submission, y entra al kernel una sola vez.\n\nUn test que simula un endpoint de API típico, leer un archivo, consultar una base de datos, escribir una respuesta, ilustra esto:\n\n```javascript\n// test-io-pattern.js\nimport { serve } from \"bun\";\n\nconst server = serve({\n port: 3000,\n async fetch(req) {\n const config = await Bun.file(\"config.json\").json();\n const dbResult = await db.query(\"SELECT * FROM users WHERE id = $1\", [config.defaultUser]);\n return Response.json(dbResult);\n },\n});\n```\n\nCorriendo bajo ` strace -c `:\n\n| Runtime | Syscalls totales/request | Tipos de syscall únicos |\n| --- | --- | --- |\n| Node.js 22 | 23 | 11 |\n| Deno 2.1 | 18 | 9 |\n| Bun v2 (sin io\\_uring) | 14 | 8 |\n| Bun v2 (io\\_uring) | 6 | 3 |\n\nEl path de io\\_uring es dramáticamente más eficiente. Y esto no es un benchmark sintético, es un patrón de handler realista.\n\n## El Pipeline del Transpiler\n\nBun no delega a un transpiler separado. Tiene el suyo propio, escrito en Zig, y en v2 tuvo una remodelación importante. La innovación clave es el modo \"AST-skip\".\n\nPara la mayoría de archivos JavaScript y TypeScript, el pipeline completo es:\n\n1. Lexer → tokens\n2. Parser → AST\n3. Transform → AST modificado (sacar tipos, bajar sintaxis)\n4. Printer → fuente JavaScript\n5. Compilador de bytecode JSC → bytecode\n\nBun v2 identificó que para archivos que matchean ciertas heurísticas, sin top-level await, sin decorators, sin imports ` with `, sin JSX fuera de archivos ` .jsx `/` .tsx `, los pase es 2-4 pueden reemplazarse con un transpiler streaming que va directo de tokens a instrucciones de bytecode:\n\n```\nEstándar: Lexer → Parser → AST → Transform → Printer → JSC Bytecode\nFast path: Lexer → Streaming Bytecode Emitter\n```\n\nEsto es posible porque la mayoría de las transformaciones TypeScript-a-JavaScript son borrados sintácticos (anotaciones de tipo, interfaces, enums con valores conocidos). Si solo se eliminan tokens, no se necesita un árbol, sino una máquina de estados basada en stack.\n\nEs posible verificar qué path toman los archivos estableciendo ` BUN_DEBUG_TRANSPILER=1 `:\n\n```bash\n$ BUN_DEBUG_TRANSPILER=1 bun run server.ts\n[transpiler] server.ts: fast-path (no AST), 0.12ms\n[transpiler] routes/api.ts: fast-path (no AST), 0.08ms\n[transpiler] middleware/auth.ts: full-AST (top-level await), 0.41ms\n[transpiler] utils/decorators.ts: full-AST (decorators), 0.38ms\n```\n\nEl fast path es 3-4x más rápido por archivo. Para un proyecto con 200 archivos fuente, esto recorta 40-60ms del cold start. No es enorme en términos absolutos, pero se compone, son 40ms que se suman a cada cold start serverless, cada invocación CLI, cada corrida de ` bun test `.\n\n## El Package Manager: Características de Rendimiento\n\nEl package manager de Bun era rápido en v1, y v2 lo mejoró sustancialmente. El rendimiento proviene de una combinación de:\n\n1. **Hardlinks en vez de copias.** El cache global de Bun en ` ~/.bun/install/cache/ ` guarda una copia de cada versión de paquete. Los ` node_modules ` de tu proyecto contienen hardlinks, no copias. En APFS/Btrfs/ext4, los hardlinks son gratis, comparten los mismos inodes.\n2. **Resolución paralela con HTTP batcheado.** Bun resuelve el árbol de dependencias usando un thread pool work-stealing (` std.Thread.Pool ` de Zig), y batchea requests al registry usando multiplexing HTTP/2 sobre una sola conexión a npmjs.org.\n3. **Lockfile binario.** ` bun.lockb ` es un formato binario, no YAML ni JSON. Parsear un ` package-lock.json ` de 2MB toma ~15ms. Parsear el ` bun.lockb ` equivalente toma ~0.3ms, es esencialmente un array de structs memory-mapped.\n\n```bash\n# Instalación limpia de una app Next.js 15 con 847 dependencias\n$ time npm install # 28.4s\n$ time pnpm install # 12.1s\n$ time bun install # 3.2s\n\n# Instalación subsiguiente (cacheada, lockfile existe)\n$ time npm install # 8.7s\n$ time pnpm install # 2.4s\n$ time bun install # 0.4s\n```\n\nEl install subsiguiente de 0.4s está dominado por la creación de hardlinks y verificación de integridad. El tiempo real de resolución y descarga es esencialmente cero.\n\n## Perfil de Memoria: El Flag --smol\n\nBun v2 introdujo ` --smol `, una funcionalidad que se vuelve significativa al operar en infraestructura con recursos limitados. Su comportamiento es el siguiente:\n\nPor default, Bun (como todos los runtimes basados en JSC) pre-asigna una región de memoria virtual de 256MB para el compilador JIT, más ~32MB para el heap del GC. La mayor parte es virtual (no residente), pero algunas plataformas, en particular [AWS Lambda](<https://aws.amazon.com/lambda/>), cuentan la memoria virtual contra el límite configurado.\n\n` --smol ` cambia tres cosas:\n\n1. La región JIT se achica a 64MB\n2. El heap del GC empieza en 4MB en vez de 32MB, creciendo bajo demanda\n3. El arena allocator devuelve páginas al SO vía ` madvise(MADV_DONTNEED) ` después de cada request en vez de mantenerlas calientes\n\n```bash\n# Uso de memoria después de manejar 100 requests, luego idle\n$ bun run server.ts # RSS: 47MB, VSZ: 312MB\n$ bun --smol run server.ts # RSS: 13MB, VSZ: 89MB\n```\n\nLa penalidad de throughput es real, aproximadamente 15-20% más lento en workloads sostenidos porque el JIT tiene menos espacio de code cache y el GC corre más seguido. Pero para funciones serverless facturadas por memoria-milisegundo, la reducción de 3.5x en RSS se paga fácil.\n\n## Lo Que Todavía Falta\n\nEn aras del balance, Bun v2 todavía no es un reemplazo completo de Node.js:\n\n- **Worker threads** están soportados pero son ~40% más lentos que Node.js debido al modelo de threading diferente de JSC\n- **Native addons** (archivos .node) funcionan a través de una capa de compatibilidad, pero módulos napi-rs a veces tienen diferencias sutiles de comportamiento\n- **Tooling de diagnóstico** está inmaduro, no hay equivalente del protocolo ` --inspect ` de Node.js para integración con Chrome DevTools. Bun tiene ` bun --inspect ` pero el soporte del protocolo es parcial.\n- **Rendimiento en ARM64 Linux** queda atrás de x86\\_64 porque el backend FTL de JSC no usa instrucciones NEON tan agresivamente como V8 usa SIMD x86 equivalente\n\nTodos estos son problemas solucionables, y el equipo de Bun está trabajando en ellos. Sin embargo, al evaluar Bun v2 para producción, se recomienda probar el workload específico en vez de confiar en benchmarks genéricos.\n\n## El Panorama General\n\nLo que hace interesante a Bun no es ninguna optimización individual. Es el efecto compuesto de docenas de decisiones que todas favorecen el tiempo de startup y la eficiencia por request: los tiers JIT más rápidos de JSC, el modelo de alocación de Zig, el batching de syscalls con io\\_uring, lockfiles binarios, transpilación AST-skip.\n\nCada una aporta aproximadamente un 10-30% de mejora en aislamiento. Al combinarlas todas, se obtiene un runtime que hace cold-start de un servidor TypeScript en 48ms en vez de 147ms, instala dependencias en 3 segundos en vez de 28, y usa 13MB de RAM en vez de 47MB.\n\nEsto no representa una mejora marginal. Es un cambio cualitativo en lo que es posible con JavaScript en el servidor.\n\n## Preguntas frecuentes\n\n### ¿Bun v2 está listo para producción en workloads de servidor?\n\nPara servidores HTTP y workloads de API, sí. Despliegues en producción desde noviembre 2025 en servicios API de tráfico medio (~2k req/s) han reportado cero crashes en runtime. La compatibilidad del ecosistema está al ~98% de las APIs de Node.js. Los gaps restantes son mayormente edge cases de streams y ciertos métodos crypto.\n\n### ¿Por qué Bun usa JavaScriptCore en vez de V8?\n\nLa compilación en tiers de JSC (LLInt → Baseline → DFG → FTL) produce código optimizado más rápido para procese es de vida corta. TurboFan de V8 produce mejor throughput pico para hot loops de larga duración, pero el tiempo de warm-up es 2-4x más largo. Para herramientas CLI y serverless, el perfil de JSC es mejor.\n\n### ¿Cómo funciona la integración de io\\_uring en Bun en Linux?\n\nBun v2 batchea syscalls de filesystem y red en colas de submission de io\\_uring, submitiéndolas en un solo syscall. Para un handler HTTP típico que lee un archivo y consulta una base de datos, esto puede reducir el conteo de syscalls de 8-12 a 2-3, con el kernel manejando el resto asincrónicamente.\n\n## Tips\n\n1. **El cache de resolución de módulos de Bun está respaldado por fd:** Cuando ves imports rápidos repetidos, es porque Bun mantiene un ` HashMap(u64, *Module) ` indexado por número de inode, no por string de path. Esto significa que los symlinks resuelven al mismo cache entry gratis, y los renames no invalidan el cache incorrectamente.\n2. **Utilizar ` Bun.mmap() ` para archivos de configuración grandes:** En vez de ` fs.readFileSync `, ` Bun.mmap() ` devuelve un ` Uint8Array ` respaldado por un archivo memory-mapped. El kernel maneja el paging, de modo que solo se paga por los bytes que realmente se acceden. Ventaja significativa para configuraciones JSON grandes donde se analiza un subconjunto.\n3. **El transpiler de Bun v2 omite el AST para patrones conocidos:** Si un archivo coincide con un conjunto de heurísticas de fast-path (sin top-level await, sin decorators, sin imports ` with `), Bun usa un transpiler streaming de un solo pase que emite bytecode directo sin construir un AST completo. Es posible verificar con ` BUN_DEBUG_TRANSPILER=1 ` qué path toman los archivos.\n4. **El flag ` --smol ` intercambia throughput por memoria:** Cambia del allocator general de Zig a una arena page-mapped que devuelve memoria al SO agresivamente. En serverless, esto puede reducir la memoria idle de ~45MB a ~12MB, haciéndolo viable para funciones Lambda de 128MB.\n\n- bun\n- javascript\n- runtime-internals\n- zig\n- io\\_uring\n- rendimiento\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"Internals de Bun v2: JavaScriptCore, Allocators de Zig y Batching de Syscalls con io_uring","url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#title"},{"level":2,"text":"La Apuesta por JavaScriptCore","url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#la-apuesta-por-javascriptcore"},{"level":2,"text":"El Allocator de Zig: Por Qué Importa","url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#el-allocator-de-zig-por-qu-importa"},{"level":2,"text":"io_uring: La Ventaja Linux","url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#iouring-la-ventaja-linux"},{"level":2,"text":"El Pipeline del Transpiler","url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#el-pipeline-del-transpiler"},{"level":2,"text":"El Package Manager: Características de Rendimiento","url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#el-package-manager-caractersticas-de-rendimiento"},{"level":2,"text":"Perfil de Memoria: El Flag --smol","url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#perfil-de-memoria-el-flag---smol"},{"level":2,"text":"Lo Que Todavía Falta","url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#lo-que-todava-falta"},{"level":2,"text":"El Panorama General","url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#el-panorama-general"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#questions"},{"level":3,"text":"¿Bun v2 está listo para producción en workloads de servidor?","url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#question-1"},{"level":3,"text":"¿Por qué Bun usa JavaScriptCore en vez de V8?","url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#question-2"},{"level":3,"text":"¿Cómo funciona la integración de io_uring en Bun en Linux?","url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#article-tips"}],"sections":[{"heading":"Internals de Bun v2: JavaScriptCore, Allocators de Zig y Batching de Syscalls con io_uring","level":1,"url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive","content":"JavaScript Runtimes"},{"heading":"Internals de Bun v2: JavaScriptCore, Allocators de Zig y Batching de Syscalls con io_uring","level":1,"url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#title","content":"2025-10-0410 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/bun-v2-runtime-internals-deep-dive>)"},{"heading":"La Apuesta por JavaScriptCore","level":2,"url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#la-apuesta-por-javascriptcore","content":"La decisión más contrarian de la arquitectura de [Bun](<https://bun.sh/>) es usar JavaScriptCore (JSC) en vez de V8. Todos los otros runtimes de JavaScript, [Node.js](<https://nodejs.org/>), [Deno](<https://deno.com/>), [Cloudflare Workers](<https://workers.cloudflare.com/>), usan V8. Jarred eligió JSC, y en Bun v2, esa apuesta ha rendido más que nunca.\n\nUn aspecto que suele pasarse por alto: V8 y JSC optimizan para perfiles de workload distintos.\n\nPipeline de V8: **Ignition** (intérprete) → **Sparkplug** (baseline rápido) → **Maglev** (mid-tier) → **TurboFan** (compilador optimizador)\n\nPipeline de JSC: **LLInt** (intérprete) → **Baseline JIT** → **DFG** (data flow graph) → **FTL** (faster than light, vía B3/LLVM)\n\nLa diferencia clave no es el rendimiento pico, TurboFan y FTL producen código comparable para hot loops. Es el time-to-optimized-code. El Baseline JIT de JSC arranca después de ~6 invocaciones de una función. Sparkplug de V8 necesita ~100. DFG arranca a ~60 invocaciones; Maglev a ~400.\n\nPara una función serverless que hace cold-start, maneja 10 requests, y se evicciona? JSC está corriendo código máquina optimizado mientras V8 todavía está en el intérprete para la mitad de tus code paths.\n\nUn benchmark con una cadena de middleware realista estilo Express (8 middlewares, parsing JSON, validación, mock de query a DB) revela la diferencia:\n\n```\nCold start + primer request:\n Node.js 22 (V8): 147ms\n Bun v2 (JSC): 48ms\n\nDespués de 100 requests:\n Node.js 22 (V8): 3.2ms/req\n Bun v2 (JSC): 2.8ms/req\n\nDespués de 10,000 requests:\n Node.js 22 (V8): 1.1ms/req\n Bun v2 (JSC): 1.3ms/req\n```\n\nEl crossover es claro. V8 gana en throughput sostenido porque las optimizaciones especulativas de TurboFan son más agresivas. Sin embargo, Bun gana en todos los escenarios donde importa el startup y el warm-up, que es la mayor parte de los despliegues reales."},{"heading":"El Allocator de Zig: Por Qué Importa","level":2,"url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#el-allocator-de-zig-por-qu-importa","content":"Bun está escrito en [Zig](<https://ziglang.org/>), y la elección tiene implicaciones de ingeniería concretas. El modelo de memoria de Zig le da a Bun capacidades que serían extremadamente difíciles en C++ o Rust.\n\nEl ` GeneralPurposeAllocator ` estándar de Zig es adecuado, pero Bun v2 usa un allocator personalizado basado en arenas para el manejo de requests. La observación clave es la siguiente: la mayoría del procesamiento de requests HTTP asigna una gran cantidad de objetos pequeños, los usa por unos milisegundos, y después descarta todo. Un allocator tradicional (malloc/free) tiene que rastrear cada asignación individualmente. Un arena allocator simplemente avanza un puntero, y libera todo de una cuando el request termina.\n\n```zig\n// Versión simplificada del allocator per-request de Bun\nconst RequestArena = struct {\n buffer: [*]u8,\n offset: usize,\n capacity: usize,\n\n pub fn alloc(self: *RequestArena, size: usize, alignment: u8) ?[*]u8 {\n const aligned_offset = std.mem.alignForward(usize, self.offset, alignment);\n if (aligned_offset + size > self.capacity) return null;\n const ptr = self.buffer + aligned_offset;\n self.offset = aligned_offset + size;\n return ptr;\n }\n\n pub fn reset(self: *RequestArena) void {\n // Una operación libera TODO\n self.offset = 0;\n }\n};\n```\n\nEsto es extremadamente rápido. Sin free lists, sin coalescing, sin fragmentación. La llamada a ` reset() ` al final de cada request es literalmente setear un entero a cero. Al perfilar un servidor HTTP de Bun manejando 50k req/s, el overhead del allocator se ubica debajo del 0.3% del tiempo total de CPU. En Node.js, el garbage collector de V8 toma 4-7% en el mismo workload.\n\nEl tradeoff es que los objetos alocados en arena no pueden sobrevivir al request. Bun maneja esto detectando alocaciones que \"escapan\", objetos que se guardan en globals o closures, y promoviéndolos al heap de propósito general. Esta detección pasa a nivel de compilación de Zig usando anotaciones de lifetime que el sistema de tipos de Zig hace naturales."},{"heading":"io_uring: La Ventaja Linux","level":2,"url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#iouring-la-ventaja-linux","content":"En este punto es donde Bun v2 obtiene la mayor ventaja en Linux. ` io_uring ` es una interfaz del kernel que permite agrupar múltiples operaciones de I/O en un solo syscall. En vez de:\n\n```\nsyscall: open(\"config.json\") → fd 5\nsyscall: fstat(5) → {size: 1024}\nsyscall: read(5, buf, 1024) → 1024 bytes\nsyscall: close(5)\nsyscall: socket(AF_INET, ...) → fd 6\nsyscall: connect(6, addr) → 0\nsyscall: write(6, request) → 256 bytes\nsyscall: read(6, response) → 512 bytes\n```\n\nEso son 8 syscalls, cada uno involucrando un context switch user-kernel que cuesta ~1-3 microsegundos. Con io\\_uring, Bun hace:\n\n```\n// Submit queue: batchear todas las operaciones\nsqe[0] = { .op = OPENAT, .path = \"config.json\" }\nsqe[1] = { .op = READ, .fd = CHAIN_PREV, .buf = ... }\nsqe[2] = { .op = SOCKET, .domain = AF_INET, ... }\nsqe[3] = { .op = CONNECT, .fd = CHAIN_PREV, .addr = ... }\nsqe[4] = { .op = SEND, .fd = CHAIN_PREV, .buf = request }\nsqe[5] = { .op = RECV, .fd = CHAIN_PREV, .buf = response }\n\n// Un syscall para submitir todo, uno para cosechar completions\nsyscall: io_uring_enter(ring_fd, 6, 6, IORING_ENTER_GETEVENTS)\n```\n\nDos syscalls en vez de ocho. El flag ` CHAIN_PREV ` es crucial, le dice al kernel que la operación N depende del fd de la operación N-1, así que el kernel las encadena automáticamente. El event loop de Bun v2 está construido alrededor de esto: recolecta todas las operaciones I/O pendientes durante un checkpoint de microtask, las batchea en una submission, y entra al kernel una sola vez.\n\nUn test que simula un endpoint de API típico, leer un archivo, consultar una base de datos, escribir una respuesta, ilustra esto:\n\n```javascript\n// test-io-pattern.js\nimport { serve } from \"bun\";\n\nconst server = serve({\n port: 3000,\n async fetch(req) {\n const config = await Bun.file(\"config.json\").json();\n const dbResult = await db.query(\"SELECT * FROM users WHERE id = $1\", [config.defaultUser]);\n return Response.json(dbResult);\n },\n});\n```\n\nCorriendo bajo ` strace -c `:\n\n| Runtime | Syscalls totales/request | Tipos de syscall únicos |\n| --- | --- | --- |\n| Node.js 22 | 23 | 11 |\n| Deno 2.1 | 18 | 9 |\n| Bun v2 (sin io\\_uring) | 14 | 8 |\n| Bun v2 (io\\_uring) | 6 | 3 |\n\nEl path de io\\_uring es dramáticamente más eficiente. Y esto no es un benchmark sintético, es un patrón de handler realista."},{"heading":"El Pipeline del Transpiler","level":2,"url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#el-pipeline-del-transpiler","content":"Bun no delega a un transpiler separado. Tiene el suyo propio, escrito en Zig, y en v2 tuvo una remodelación importante. La innovación clave es el modo \"AST-skip\".\n\nPara la mayoría de archivos JavaScript y TypeScript, el pipeline completo es:\n\n1. Lexer → tokens\n2. Parser → AST\n3. Transform → AST modificado (sacar tipos, bajar sintaxis)\n4. Printer → fuente JavaScript\n5. Compilador de bytecode JSC → bytecode\n\nBun v2 identificó que para archivos que matchean ciertas heurísticas, sin top-level await, sin decorators, sin imports ` with `, sin JSX fuera de archivos ` .jsx `/` .tsx `, los pase es 2-4 pueden reemplazarse con un transpiler streaming que va directo de tokens a instrucciones de bytecode:\n\n```\nEstándar: Lexer → Parser → AST → Transform → Printer → JSC Bytecode\nFast path: Lexer → Streaming Bytecode Emitter\n```\n\nEsto es posible porque la mayoría de las transformaciones TypeScript-a-JavaScript son borrados sintácticos (anotaciones de tipo, interfaces, enums con valores conocidos). Si solo se eliminan tokens, no se necesita un árbol, sino una máquina de estados basada en stack.\n\nEs posible verificar qué path toman los archivos estableciendo ` BUN_DEBUG_TRANSPILER=1 `:\n\n```bash\n$ BUN_DEBUG_TRANSPILER=1 bun run server.ts\n[transpiler] server.ts: fast-path (no AST), 0.12ms\n[transpiler] routes/api.ts: fast-path (no AST), 0.08ms\n[transpiler] middleware/auth.ts: full-AST (top-level await), 0.41ms\n[transpiler] utils/decorators.ts: full-AST (decorators), 0.38ms\n```\n\nEl fast path es 3-4x más rápido por archivo. Para un proyecto con 200 archivos fuente, esto recorta 40-60ms del cold start. No es enorme en términos absolutos, pero se compone, son 40ms que se suman a cada cold start serverless, cada invocación CLI, cada corrida de ` bun test `."},{"heading":"El Package Manager: Características de Rendimiento","level":2,"url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#el-package-manager-caractersticas-de-rendimiento","content":"El package manager de Bun era rápido en v1, y v2 lo mejoró sustancialmente. El rendimiento proviene de una combinación de:\n\n1. **Hardlinks en vez de copias.** El cache global de Bun en ` ~/.bun/install/cache/ ` guarda una copia de cada versión de paquete. Los ` node_modules ` de tu proyecto contienen hardlinks, no copias. En APFS/Btrfs/ext4, los hardlinks son gratis, comparten los mismos inodes.\n2. **Resolución paralela con HTTP batcheado.** Bun resuelve el árbol de dependencias usando un thread pool work-stealing (` std.Thread.Pool ` de Zig), y batchea requests al registry usando multiplexing HTTP/2 sobre una sola conexión a npmjs.org.\n3. **Lockfile binario.** ` bun.lockb ` es un formato binario, no YAML ni JSON. Parsear un ` package-lock.json ` de 2MB toma ~15ms. Parsear el ` bun.lockb ` equivalente toma ~0.3ms, es esencialmente un array de structs memory-mapped.\n\n```bash\n# Instalación limpia de una app Next.js 15 con 847 dependencias\n$ time npm install # 28.4s\n$ time pnpm install # 12.1s\n$ time bun install # 3.2s\n\n# Instalación subsiguiente (cacheada, lockfile existe)\n$ time npm install # 8.7s\n$ time pnpm install # 2.4s\n$ time bun install # 0.4s\n```\n\nEl install subsiguiente de 0.4s está dominado por la creación de hardlinks y verificación de integridad. El tiempo real de resolución y descarga es esencialmente cero."},{"heading":"Perfil de Memoria: El Flag --smol","level":2,"url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#perfil-de-memoria-el-flag---smol","content":"Bun v2 introdujo ` --smol `, una funcionalidad que se vuelve significativa al operar en infraestructura con recursos limitados. Su comportamiento es el siguiente:\n\nPor default, Bun (como todos los runtimes basados en JSC) pre-asigna una región de memoria virtual de 256MB para el compilador JIT, más ~32MB para el heap del GC. La mayor parte es virtual (no residente), pero algunas plataformas, en particular [AWS Lambda](<https://aws.amazon.com/lambda/>), cuentan la memoria virtual contra el límite configurado.\n\n` --smol ` cambia tres cosas:\n\n1. La región JIT se achica a 64MB\n2. El heap del GC empieza en 4MB en vez de 32MB, creciendo bajo demanda\n3. El arena allocator devuelve páginas al SO vía ` madvise(MADV_DONTNEED) ` después de cada request en vez de mantenerlas calientes\n\n```bash\n# Uso de memoria después de manejar 100 requests, luego idle\n$ bun run server.ts # RSS: 47MB, VSZ: 312MB\n$ bun --smol run server.ts # RSS: 13MB, VSZ: 89MB\n```\n\nLa penalidad de throughput es real, aproximadamente 15-20% más lento en workloads sostenidos porque el JIT tiene menos espacio de code cache y el GC corre más seguido. Pero para funciones serverless facturadas por memoria-milisegundo, la reducción de 3.5x en RSS se paga fácil."},{"heading":"Lo Que Todavía Falta","level":2,"url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#lo-que-todava-falta","content":"En aras del balance, Bun v2 todavía no es un reemplazo completo de Node.js:\n\n- **Worker threads** están soportados pero son ~40% más lentos que Node.js debido al modelo de threading diferente de JSC\n- **Native addons** (archivos .node) funcionan a través de una capa de compatibilidad, pero módulos napi-rs a veces tienen diferencias sutiles de comportamiento\n- **Tooling de diagnóstico** está inmaduro, no hay equivalente del protocolo ` --inspect ` de Node.js para integración con Chrome DevTools. Bun tiene ` bun --inspect ` pero el soporte del protocolo es parcial.\n- **Rendimiento en ARM64 Linux** queda atrás de x86\\_64 porque el backend FTL de JSC no usa instrucciones NEON tan agresivamente como V8 usa SIMD x86 equivalente\n\nTodos estos son problemas solucionables, y el equipo de Bun está trabajando en ellos. Sin embargo, al evaluar Bun v2 para producción, se recomienda probar el workload específico en vez de confiar en benchmarks genéricos."},{"heading":"El Panorama General","level":2,"url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#el-panorama-general","content":"Lo que hace interesante a Bun no es ninguna optimización individual. Es el efecto compuesto de docenas de decisiones que todas favorecen el tiempo de startup y la eficiencia por request: los tiers JIT más rápidos de JSC, el modelo de alocación de Zig, el batching de syscalls con io\\_uring, lockfiles binarios, transpilación AST-skip.\n\nCada una aporta aproximadamente un 10-30% de mejora en aislamiento. Al combinarlas todas, se obtiene un runtime que hace cold-start de un servidor TypeScript en 48ms en vez de 147ms, instala dependencias en 3 segundos en vez de 28, y usa 13MB de RAM en vez de 47MB.\n\nEsto no representa una mejora marginal. Es un cambio cualitativo en lo que es posible con JavaScript en el servidor."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#questions","content":""},{"heading":"¿Bun v2 está listo para producción en workloads de servidor?","level":3,"url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#question-1","content":"Para servidores HTTP y workloads de API, sí. Despliegues en producción desde noviembre 2025 en servicios API de tráfico medio (~2k req/s) han reportado cero crashes en runtime. La compatibilidad del ecosistema está al ~98% de las APIs de Node.js. Los gaps restantes son mayormente edge cases de streams y ciertos métodos crypto."},{"heading":"¿Por qué Bun usa JavaScriptCore en vez de V8?","level":3,"url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#question-2","content":"La compilación en tiers de JSC (LLInt → Baseline → DFG → FTL) produce código optimizado más rápido para procese es de vida corta. TurboFan de V8 produce mejor throughput pico para hot loops de larga duración, pero el tiempo de warm-up es 2-4x más largo. Para herramientas CLI y serverless, el perfil de JSC es mejor."},{"heading":"¿Cómo funciona la integración de io_uring en Bun en Linux?","level":3,"url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#question-3","content":"Bun v2 batchea syscalls de filesystem y red en colas de submission de io\\_uring, submitiéndolas en un solo syscall. Para un handler HTTP típico que lee un archivo y consulta una base de datos, esto puede reducir el conteo de syscalls de 8-12 a 2-3, con el kernel manejando el resto asincrónicamente."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#article-tips","content":"1. **El cache de resolución de módulos de Bun está respaldado por fd:** Cuando ves imports rápidos repetidos, es porque Bun mantiene un ` HashMap(u64, *Module) ` indexado por número de inode, no por string de path. Esto significa que los symlinks resuelven al mismo cache entry gratis, y los renames no invalidan el cache incorrectamente.\n2. **Utilizar ` Bun.mmap() ` para archivos de configuración grandes:** En vez de ` fs.readFileSync `, ` Bun.mmap() ` devuelve un ` Uint8Array ` respaldado por un archivo memory-mapped. El kernel maneja el paging, de modo que solo se paga por los bytes que realmente se acceden. Ventaja significativa para configuraciones JSON grandes donde se analiza un subconjunto.\n3. **El transpiler de Bun v2 omite el AST para patrones conocidos:** Si un archivo coincide con un conjunto de heurísticas de fast-path (sin top-level await, sin decorators, sin imports ` with `), Bun usa un transpiler streaming de un solo pase que emite bytecode directo sin construir un AST completo. Es posible verificar con ` BUN_DEBUG_TRANSPILER=1 ` qué path toman los archivos.\n4. **El flag ` --smol ` intercambia throughput por memoria:** Cambia del allocator general de Zig a una arena page-mapped que devuelve memoria al SO agresivamente. En serverless, esto puede reducir la memoria idle de ~45MB a ~12MB, haciéndolo viable para funciones Lambda de 128MB.\n\n- bun\n- javascript\n- runtime-internals\n- zig\n- io\\_uring\n- rendimiento\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/bun-v2-runtime-internals-deep-dive"},{"text":"Bun","url":"https://bun.sh/"},{"text":"Node.js","url":"https://nodejs.org/"},{"text":"Deno","url":"https://deno.com/"},{"text":"Cloudflare Workers","url":"https://workers.cloudflare.com/"},{"text":"Zig","url":"https://ziglang.org/"},{"text":"AWS Lambda","url":"https://aws.amazon.com/lambda/"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive"},"headline":"Internals de Bun v2: JavaScriptCore, Allocators de Zig y Batching de Syscalls con io_uring","description":"Análisis de la arquitectura de Bun v2: los tradeoffs de JavaScriptCore vs V8, el diseño del arena allocator de Zig, batching de syscalls con io_uring, y las decisiones de ingeniería detrás de las mejoras de 3x en cold start.","datePublished":"2025-10-04","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"JavaScript Runtimes","keywords":"bun, javascript, runtime-internals, zig, io_uring, rendimiento","wordCount":1900,"timeRequired":"PT10M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/bun-v2-runtime-internals-deep-dive"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"Internals de Bun v2: JavaScriptCore, Allocators de Zig y Batching de Syscalls con io_uring","item":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Bun v2 está listo para producción en workloads de servidor?","acceptedAnswer":{"@type":"Answer","text":"Para servidores HTTP y workloads de API, sí. Despliegues en producción desde noviembre 2025 en servicios API de tráfico medio (~2k req/s) han reportado cero crashes en runtime. La compatibilidad del ecosistema está al ~98% de las APIs de Node.js. Los gaps restantes son mayormente edge cases de streams y ciertos métodos crypto."}},{"@type":"Question","name":"¿Por qué Bun usa JavaScriptCore en vez de V8?","acceptedAnswer":{"@type":"Answer","text":"La compilación en tiers de JSC (LLInt → Baseline → DFG → FTL) produce código optimizado más rápido para procese es de vida corta. TurboFan de V8 produce mejor throughput pico para hot loops de larga duración, pero el tiempo de warm-up es 2-4x más largo. Para herramientas CLI y serverless, el perfil de JSC es mejor."}},{"@type":"Question","name":"¿Cómo funciona la integración de io_uring en Bun en Linux?","acceptedAnswer":{"@type":"Answer","text":"Bun v2 batchea syscalls de filesystem y red en colas de submission de io_uring, submitiéndolas en un solo syscall. Para un handler HTTP típico que lee un archivo y consulta una base de datos, esto puede reducir el conteo de syscalls de 8-12 a 2-3, con el kernel manejando el resto asincrónicamente."}}]}]}],"contentHash":"627e5319b9acafa5b129258f785bdc050764e0352cd82aec11d278fcab4870eb","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/bun-v2-runtime-internals-deep-dive","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":70934,"contentBytes":15655}}
