No history yet

Arquitectura del Motor JS

Dentro del motor de JavaScript

Cuando ejecutas código JavaScript, no estás simplemente enviando comandos al aire. Un programa sofisticado llamado motor de JavaScript lee, interpreta y ejecuta tu código. El más conocido es el V8 de Google, que impulsa a Chrome y Node.js, pero otros como SpiderMonkey (Firefox) y JavaScriptCore (Safari) funcionan con principios similares.

El motor tiene dos componentes principales para gestionar tu código: el Heap de memoria y la Pila de ejecución.

Memoria y Ejecución: Heap y Pila

Imagina que estás cocinando. El Heap de memoria es como tu despensa: un gran espacio desestructurado donde almacenas todos tus ingredientes (objetos, funciones, arrays). No hay un orden particular, solo un lugar para guardar cosas que necesitarás más tarde.

La Pila de ejecución (Call Stack), por otro lado, es tu receta. Es una lista ordenada, paso a paso, de las tareas que estás realizando. Si una tarea principal (una función) requiere una subtarea (otra función), la añades a la parte superior de la lista. Cuando terminas la subtarea, la tachas y vuelves a la anterior. Sigue una regla estricta: "último en entrar, primero en salir" (LIFO).

Cada vez que se invoca una función, se crea un nuevo "marco" (stack frame) en la parte superior de la pila. Este marco contiene los argumentos de la función y sus variables locales. Cuando la función retorna, su marco se elimina de la pila.

function funcionB() {
  // La pila ahora contiene: main -> funcionA -> funcionB
  console.log('Tercero');
}

function funcionA() {
  // La pila ahora contiene: main -> funcionA
  console.log('Segundo');
  funcionB();
}

// El script principal (main) se añade a la pila.
console.log('Primero');
funcionA();
// Salida: Primero, Segundo, Tercero

Este modelo funciona perfectamente para código síncrono. Pero JavaScript está diseñado para entornos como los navegadores, donde no podemos permitirnos esperar por operaciones lentas como una llamada a una API o la acción de un usuario. Bloquear la pila significaría una interfaz congelada. Aquí es donde entra en juego el Bucle de Eventos.

El Bucle de Eventos: Microtareas vs. Macrotareas

El Bucle de Eventos (Event Loop) es el mecanismo que permite a JavaScript realizar operaciones asíncronas sin bloquear el hilo principal. Cuando te encuentras con una operación asíncrona como setTimeout o una petición fetch, el motor de JS no espera. La delega a un entorno externo (como el navegador) y continúa ejecutando el código que sigue.

Una vez que la operación asíncrona finaliza, su función de callback no vuelve directamente a la Pila de Ejecución. En su lugar, se coloca en una de dos colas:

  1. Cola de Macrotareas (Task Queue): Contiene callbacks de eventos como setTimeout, setInterval, I/O (lectura de archivos, peticiones de red) y eventos de usuario (clics, pulsaciones de teclas).

  2. Cola de Microtareas (Microtask Queue): Contiene callbacks de Promise.then(), .catch(), .finally(), async/await y queueMicrotask(). Es una cola de alta prioridad.

La regla de oro del Bucle de Eventos: después de que cada macrotarea finaliza, el motor ejecuta todas las microtareas de la cola de microtareas antes de procesar la siguiente macrotarea.

Esto significa que las microtareas siempre se ejecutarán antes que las macrotareas, lo que puede tener un gran impacto en el orden de ejecución y la capacidad de respuesta de tu aplicación. Una promesa resuelta siempre se ejecutará antes que un setTimeout con un retardo de 0 milisegundos.

console.log('Inicio del script'); // 1. Síncrono

setTimeout(() => {
  console.log('Timeout (Macrotarea)'); // 5. Macrotarea
}, 0);

Promise.resolve().then(() => {
  console.log('Promesa resuelta (Microtarea)'); // 3. Microtarea
});

queueMicrotask(() => {
  console.log('queueMicrotask (Microtarea)'); // 4. Microtarea
});

console.log('Fin del script'); // 2. Síncrono

// Salida esperada:
// Inicio del script
// Fin del script
// Promesa resuelta (Microtarea)
// queueMicrotask (Microtarea)
// Timeout (Macrotarea)

Comprender esta jerarquía es clave para depurar comportamientos asíncronos inesperados y para optimizar la renderización en el navegador, asegurando que las actualizaciones críticas de la UI (a menudo gestionadas con microtareas) ocurran lo antes posible.

Gestión de la memoria

El Heap no es infinito. Tarde o temprano, el espacio ocupado por objetos que ya no se necesitan debe liberarse. Este proceso se llama recolección de basura (Garbage Collection).

Los motores modernos utilizan recolectores de basura sofisticados. El enfoque más común es un algoritmo de "marcar y barrer" (mark-and-sweep). El recolector comienza desde un conjunto de objetos "raíz" (como el objeto global) y recorre todas las referencias para encontrar todos los objetos alcanzables, marcándolos como "vivos". En la fase de "barrido", cualquier objeto que no haya sido marcado se considera basura y su memoria se libera.

Una fuga de memoria (memory leak) ocurre cuando se almacenan objetos en el Heap que ya no son necesarios, pero que accidentalmente siguen siendo referenciados desde algún lugar, impidiendo que el recolector de basura los elimine.

Para manejar escenarios complejos, especialmente al asociar metadatos a objetos sin impedir que sean recolectados, JavaScript introdujo las estructuras de memoria débil.

WeakMap

noun

Un tipo de colección similar a un Map, pero donde las claves deben ser objetos y se mantienen de forma "débil". Si un objeto usado como clave en un WeakMap ya no tiene ninguna otra referencia en el programa, el recolector de basura puede eliminarlo, y la entrada correspondiente en el WeakMap también desaparecerá.

WeakSet funciona de manera similar, pero para conjuntos de objetos. Estas estructuras son herramientas poderosas para construir cachés o registrar información sobre objetos de una manera segura para la memoria.

let obj = { id: 1 };
const weakMap = new WeakMap();

weakMap.set(obj, 'Metadatos privados');

// Aún podemos acceder a los metadatos
console.log(weakMap.has(obj)); // true

// Ahora eliminamos la única referencia fuerte a 'obj'
obj = null;

// Después de un tiempo, el recolector de basura
// eliminará el objeto original y su entrada en el WeakMap.
// No hay forma directa de comprobarlo, pero la memoria se libera.
Quiz Questions 1/6

Si ejecutas el siguiente código, ¿cuál será el orden de los mensajes en la consola?

console.log('A');

setTimeout(() => {
  console.log('B');
}, 0);

Promise.resolve().then(() => {
  console.log('C');
});

console.log('D');
Quiz Questions 2/6

¿Cuáles son los dos componentes principales que un motor de JavaScript utiliza para gestionar el código en tiempo de ejecución?

Entender cómo funciona el motor de JavaScript por dentro te permite escribir código no solo funcional, sino también eficiente y robusto, evitando cuellos de botella y fugas de memoria.