Command Palette

Search for a command to run...

Optimización del Event Loop en JavaScript

Aprende cómo funciona el event loop de JavaScript y cómo optimizar tu código para evitar bloqueos y mantener la capacidad de respuesta de tus aplicaciones.

Lectura: 20 min
Nivel: Intermedio

TL;DR - Resumen rápido

  • El event loop es el mecanismo que permite a JavaScript ejecutar código asíncrono de manera no bloqueante
  • JavaScript es single-threaded: solo puede ejecutar una tarea a la vez en el call stack
  • Las tareas largas bloquean el event loop y hacen que la interfaz de usuario se congele
  • Divide tareas pesadas en chunks más pequeños usando setTimeout(0) o requestIdleCallback
  • Usa Web Workers para ejecutar cómputos intensivos en hilos separados

Introducción al Event Loop

El event loop es el corazón de la ejecución asíncrona en JavaScript. Es el mecanismo que permite a JavaScript, que es single-threaded (un solo hilo), ejecutar operaciones asíncronas como callbacks, promises y eventos sin bloquear la ejecución del código. Comprender cómo funciona el event loop es fundamental para escribir código eficiente y evitar problemas de rendimiento que pueden hacer que tu aplicación se sienta lenta o no responda.

Aunque JavaScript es single-threaded, el event loop permite que el navegador ejecute múltiples operaciones simultáneamente usando un modelo basado en eventos. El event loop coordina la ejecución de código JavaScript, el manejo de eventos, el rendering del DOM y otras tareas del navegador en un solo hilo de ejecución.

Por Qué el Event Loop es Importante

El event loop es crítico para la experiencia de usuario porque determina qué tan responsiva se siente tu aplicación. Si el event loop está bloqueado por una tarea larga, la interfaz de usuario no puede responder a clics, scrolling o animaciones, haciendo que la aplicación se sienta congelada. Optimizar el event loop es esencial para aplicaciones web modernas.

¿Qué es el Event Loop?

El event loop es un bucle infinito que coordina la ejecución de código JavaScript en un entorno de navegador o Node.js. Su trabajo principal es monitorear el call stack (pila de llamadas) y las colas de tareas (task queues), y ejecutar tareas cuando el call stack está vacío. El event loop es lo que permite que JavaScript sea asíncrono a pesar de ser single-threaded.

El event loop funciona continuamente, verificando si hay código en el call stack. Si el call stack está vacío, el event loop toma la primera tarea de la cola de microtasks y la ejecuta. Después de procesar todas las microtasks, toma la primera tarea de la cola de macrotasks y la ejecuta. Este ciclo se repite indefinidamente, permitiendo que JavaScript maneje eventos, callbacks y operaciones asíncronas.

El event loop monitorea el call stack y las colas de tareas (microtasks y macrotasks), ejecutando tareas cuando el call stack está vacío. Esto permite que JavaScript sea asíncrono a pesar de ser single-threaded, y es directamente responsable de la capacidad de respuesta de la aplicación.

Cómo Funciona el Event Loop

El event loop coordina tres componentes principales: el call stack, la microtask queue y la macrotask queue (también llamada task queue). Comprender cómo interactúan estos componentes es fundamental para escribir código eficiente y predecible.

Call Stack

El call stack es una estructura LIFO (Last In, First Out) que mantiene registro de las llamadas a funciones que se están ejecutando actualmente. Cuando se llama a una función, se agrega al tope del stack. Cuando la función termina, se elimina del stack. El event loop solo puede ejecutar nuevas tareas cuando el call stack está vacío.

call-stack.js
Loading code...

Este ejemplo muestra cómo el call stack funciona. La función main llama a firstFunction, que llama a secondFunction. Cada llamada se agrega al call stack, y cuando una función termina, se elimina del stack. El event loop espera a que el call stack esté completamente vacío antes de procesar la siguiente tarea de las colas.

Task Queue (Macrotask Queue)

La task queue (o macrotask queue) es una cola FIFO (First In, First Out) que contiene tareas como callbacks de setTimeout, setInterval, eventos del DOM y operaciones de I/O. El event loop procesa una tarea de esta cola después de que el call stack está vacío y todas las microtasks han sido procesadas.

task-queue.js
Loading code...

Este ejemplo muestra cómo la task queue funciona. El console.log('End') se ejecuta primero porque está en el call stack. Luego, el event loop procesa las microtasks (promises) y finalmente las macrotasks (setTimeout). El orden de ejecución es: Start → End → Promise → setTimeout.

setTimeout(fn, 0) No Ejecuta Inmediatamente

setTimeout con 0ms de delay no ejecuta la función inmediatamente. La función se agrega a la task queue y se ejecuta después de que el call stack esté vacío y todas las microtasks hayan sido procesadas. Esto es útil para delegar trabajo al event loop.

Microtask Queue

La microtask queue es una cola de mayor prioridad que contiene tareas como callbacks de Promises y MutationObserver. El event loop procesa todas las microtasks después de que cada tarea del call stack termina, antes de procesar cualquier macrotask de la task queue. Esto garantiza que las microtasks se ejecuten lo más pronto posible.

microtask-queue.js
Loading code...

Este ejemplo muestra cómo las microtasks tienen prioridad sobre las macrotasks. Las promises (microtasks) se ejecutan antes de los setTimeouts (macrotasks). El orden de ejecución es: Script → Promise 1 → Promise 2 → setTimeout 1 → setTimeout 2. Las microtasks se procesan todas juntas antes de pasar a la siguiente macrotask.

Bloqueos del Event Loop

Un bloqueo del event loop ocurre cuando una tarea síncrona tarda mucho tiempo en completarse, impidiendo que el event loop procese otras tareas. Durante un bloqueo, la interfaz de usuario no responde a clics, scrolling o animaciones, y los eventos del DOM se acumulan en las colas esperando ser procesados.

Los bloqueos son especialmente problemáticos en aplicaciones web porque afectan directamente la experiencia del usuario. Una tarea que tarda más de 50ms se considera "long task" y puede causar que la animación se sienta entrecortada o que la aplicación no responda a la entrada del usuario.

blocking-task.js
Loading code...

Este ejemplo muestra una tarea bloqueante. La función heavyTask ejecuta un bucle que tarda varios segundos en completarse. Durante este tiempo, el event loop está completamente bloqueado y no puede procesar ninguna otra tarea, incluyendo eventos del DOM, callbacks de promesas o renderizado de la UI.

  • Las tareas que tardan más de 50ms se consideran 'long tasks' y pueden afectar la UX
  • Durante un bloqueo, la UI no responde a clics, scrolling o animaciones
  • Los eventos del DOM se acumulan en las colas esperando ser procesados
  • Los bloques largos pueden causar que el navegador muestre una advertencia
  • Los bloqueos son especialmente problemáticos en dispositivos móviles con CPU más lenta

Detectar Long Tasks en DevTools

En Chrome DevTools > Performance, puedes ver las "long tasks" como barras rojas en la línea de tiempo. Estas barras indican tareas que bloquearon el event loop por más de 50ms. Identifica y optimiza estas tareas para mejorar la capacidad de respuesta de tu aplicación.

Cómo Evitar Bloqueos del Event Loop

Evitar bloqueos del event loop requiere dividir tareas pesadas en chunks más pequeños que el event loop pueda procesar sin interrumpir la capacidad de respuesta de la aplicación. Hay varias técnicas para lograr esto, incluyendo el uso de setTimeout(0), requestIdleCallback, y Web Workers.

Dividir Tareas Pesadas

Una de las técnicas más efectivas para evitar bloqueos es dividir tareas pesadas en chunks más pequeños y usar setTimeout(0) para ceder control al event loop entre chunks. Esto permite que el event loop procese otras tareas (como renderizado de la UI y eventos del DOM) entre chunks.

divide-tasks.js
Loading code...

Este ejemplo muestra cómo dividir una tarea pesada en chunks más pequeños. La función processInChunks divide el trabajo en porciones de 1000 elementos y usa setTimeout(0) para ceder control al event loop entre chunks. Esto permite que la UI se mantenga responsiva mientras se procesan los datos.

Usar Web Workers

Web Workers permiten ejecutar código JavaScript en hilos separados del hilo principal. Esto es ideal para tareas computacionalmente intensivas que bloquearían el event loop si se ejecutaran en el hilo principal. Los workers se comunican con el hilo principal usando mensajes.

web-worker.js
Loading code...

Este ejemplo muestra cómo usar un Web Worker para ejecutar una tarea pesada en un hilo separado. El worker recibe datos, los procesa y envía el resultado de vuelta al hilo principal usando postMessage. Mientras el worker procesa los datos, el hilo principal permanece libre para responder a eventos del DOM y renderizar la UI.

Limitaciones de Web Workers

Los Web Workers no tienen acceso al DOM, window o document. Solo pueden ejecutar código JavaScript puro y comunicarse con el hilo principal usando mensajes. Esto los hace ideales para procesamiento de datos, cálculos matemáticos y operaciones de I/O, pero no para manipulación directa del DOM.

Optimizar Operaciones Síncronas

A veces no es posible dividir una tarea o usar workers. En estos casos, debes optimizar las operaciones síncronas para que se ejecuten lo más rápido posible. Esto incluye usar algoritmos más eficientes, evitar bucles anidados, y minimizar el acceso al DOM.

optimize-sync.js
Loading code...

Este ejemplo muestra cómo optimizar operaciones síncronas. La primera versión usa filter() y map() que crean arrays intermedios. La segunda versión usa reduce() que procesa los datos en una sola pasada, creando solo un array final. La versión optimizada es más rápida y genera menos presión de garbage collection.

Patrones Asíncronos Eficientes

Elegir el patrón asíncrono correcto puede afectar significativamente el rendimiento de tu aplicación. Las promesas, async/await y callbacks tienen diferentes características de rendimiento y casos de uso óptimos. Comprender cuándo usar cada patrón te ayudará a escribir código más eficiente.

async-patterns.js
Loading code...

Este ejemplo muestra diferentes patrones asíncronos. Promise.all() ejecuta múltiples promesas en paralelo y espera a que todas terminen, lo cual es más rápido que ejecutarlas secuencialmente. async/await hace el código más legible pero puede bloquear si no se usa correctamente con Promise.all().

  • Usa Promise.all() para ejecutar múltiples operaciones asíncronas en paralelo
  • Usa async/await para código legible, pero combina con Promise.all() para paralelismo
  • Evita anidar promesas profundamente (callback hell), usa async/await en su lugar
  • Usa Promise.race() para ejecutar la primera promesa que termine
  • Considera usar AbortController para cancelar operaciones asíncronas largas

Evitar Microtask Starvation

Evita crear microtasks en bucles que nunca terminan, ya que esto puede causar "microtask starvation" donde las macrotasks nunca se ejecutan. Si necesitas ejecutar código repetidamente, usa setTimeout o setInterval en lugar de crear promesas en un bucle infinito.

Resumen: Optimización del Event Loop

Conceptos principales:

  • El event loop coordina la ejecución de código JavaScript en un solo hilo
  • El call stack mantiene registro de las llamadas a funciones en ejecución
  • La microtask queue tiene prioridad sobre la task queue (macrotasks)
  • Las tareas que tardan más de 50ms se consideran 'long tasks' y pueden afectar la UX
  • Los bloqueos del event loop hacen que la UI no responda a eventos del usuario
  • Web Workers permiten ejecutar código en hilos separados del hilo principal

Mejores prácticas:

  • Divide tareas pesadas en chunks más pequeños usando setTimeout(0) o requestIdleCallback
  • Usa Web Workers para cómputos intensivos que bloquearían el event loop
  • Optimiza operaciones síncronas usando algoritmos eficientes y minimizando acceso al DOM
  • Usa Promise.all() para ejecutar múltiples operaciones asíncronas en paralelo
  • Evita crear microtasks en bucles que nunca terminan para prevenir microtask starvation
  • Identifica long tasks en Chrome DevTools Performance y optimízalas