Introducción a Web Workers: Ejecución Paralela en JavaScript
Aprende a ejecutar código JavaScript en hilos separados para tareas intensivas sin bloquear la interfaz de usuario, mejorando la experiencia del usuario.
TL;DR - Resumen rápido
- Los Web Workers permiten ejecutar JavaScript en hilos separados del hilo principal
- No pueden acceder al DOM, window, document ni otros objetos del hilo principal
- Se comunican mediante el sistema de mensajes usando postMessage y onmessage
- Son ideales para tareas intensivas de CPU como procesamiento de datos o cálculos matemáticos
- Debes terminar explícitamente los workers con terminate() para liberar recursos
Introducción a Web Workers
JavaScript es conocido por ser un lenguaje de un solo hilo (single-threaded), lo que significa que todo el código se ejecuta secuencialmente en el hilo principal. Esto puede causar problemas cuando tienes tareas que requieren mucho procesamiento, ya que bloquean la interfaz de usuario y hacen que la página se sienta lenta o no responda.
Los Web Workers son una solución nativa de JavaScript introducida en HTML5 (2009) que permite ejecutar código en hilos separados (background threads) sin afectar el rendimiento de la interfaz de usuario. Esta característica es especialmente útil para aplicaciones que necesitan procesar grandes cantidades de datos, realizar cálculos complejos o manejar operaciones que tardan mucho en completarse, permitiendo un verdadero paralelismo en JavaScript.
¿Qué es un Web Worker?
Un Web Worker es esencialmente un script JavaScript que se ejecuta en un hilo separado del hilo principal del navegador. Esto significa que mientras el worker está procesando datos, el hilo principal puede seguir respondiendo a las interacciones del usuario, renderizando animaciones y manteniendo la interfaz fluida.
Es importante entender que los Web Workers tienen limitaciones específicas diseñadas para mantener la seguridad y estabilidad de la aplicación. Estas restricciones también aseguran que el código del worker no pueda interferir con la interfaz de usuario.
- <strong>No pueden acceder al DOM</strong>: No pueden manipular elementos HTML ni acceder a window o document
- <strong>No tienen acceso a objetos del hilo principal</strong>: localStorage, sessionStorage, cookies no están disponibles
- <strong>Se comunican por mensajes</strong>: Usan postMessage y onmessage para intercambiar datos
- <strong>Tienen su propio contexto global</strong>: self es el objeto global en lugar de window
- <strong>Pueden importar scripts</strong>: Usan importScripts() para cargar librerías externas
- <strong>Pueden hacer peticiones de red</strong>: fetch, XMLHttpRequest funcionan normalmente
Los Web Workers son ideales para ciertos tipos de tareas, pero no son apropiados para todo. Aquí te mostramos cuándo usarlos y cuándo evitarlos:
- <strong>✅ Ideales para</strong>: Procesamiento de imágenes, análisis de datos grandes, cálculos matemáticos complejos, parseo de JSON o XML extenso
- <strong>✅ Buenos para</strong>: Cifrado/descifrado, compresión de datos, validaciones complejas, operaciones con Web Crypto API
- <strong>❌ No usar para</strong>: Manipulación del DOM, acceso a localStorage/sessionStorage, tareas simples que tardan menos de 10ms
- <strong>❌ Evitar para</strong>: Operaciones que requieren acceso frecuente a datos del hilo principal (mucha comunicación reduce el beneficio)
Limitación importante
Aunque los Web Workers son muy potentes, no son la solución para todos los problemas de rendimiento. Para tareas simples o que requieren acceso al DOM, considera usar técnicas como requestAnimationFrame, Web Animations API o simplemente optimizar tu código principal.
Crear un Web Worker
Para crear un Web Worker, necesitas dos archivos: el archivo principal que crea el worker y el archivo del worker que contiene el código que se ejecutará en el hilo separado. El worker se crea usando el constructor Worker(), que recibe como parámetro la URL del archivo del worker.
Archivo del Worker
Primero, creemos el archivo del worker. Este archivo contiene el código que se ejecutará en el hilo separado. El worker escucha mensajes usando self.onmessage y responde usando self.postMessage.
Este worker escucha mensajes entrantes, realiza un cálculo intensivo (en este caso, un bucle que simula procesamiento), y envía el resultado de vuelta al hilo principal. El uso de self.onmessage es equivalente a onmessage en el contexto del worker.
Crear el Worker desde el Hilo Principal
Ahora creemos el archivo principal que instancia y comunica con el worker. El hilo principal crea el worker, envía datos y recibe los resultados sin bloquearse.
Este código muestra cómo crear un worker, enviarle datos usando postMessage, recibir resultados a través del evento message, y manejar errores con el evento error. Nota que la interfaz de usuario permanece responsiva mientras el worker procesa los datos en segundo plano.
Mejor práctica
Siempre maneja el evento error del worker. Los errores en el worker no se propagan al hilo principal por defecto, y sin un handler adecuado, pueden causar que el worker falle silenciosamente, dejando tu aplicación en un estado inconsistente.
Comunicación Básica
La comunicación entre el hilo principal y el worker se realiza mediante el paso de mensajes. Este es un sistema asíncrono donde los datos se copian (no se comparten) entre los contextos. Esto significa que cuando envías un objeto grande, se crea una copia completa, lo que puede impactar el rendimiento para datos muy grandes.
Enviar y Recibir Mensajes
El sistema de mensajes es bidireccional: tanto el hilo principal como el worker pueden enviar y recibir mensajes. Esto permite un flujo de comunicación completo donde puedes solicitar procesamiento y recibir resultados, o incluso mantener una comunicación continua.
Este ejemplo muestra una comunicación bidireccional completa. El hilo principal envía un número, el worker lo procesa y devuelve el resultado, y luego el hilo principal puede enviar más datos. Este patrón es útil para tareas que requieren múltiples intercambios de información.
Transferir Objetos
Para mejorar el rendimiento con objetos grandes como ArrayBuffer, puedes usar el segundo parámetro de postMessage para transferir la propiedad en lugar de copiarla. Esto hace que el objeto sea transferido completamente al otro contexto, dejándolo inaccesible en el contexto original.
Al transferir objetos en lugar de copiarlos, mejoras significativamente el rendimiento para datos grandes como buffers de imágenes, archivos o arrays de datos numéricos. Sin embargo, ten en cuenta que el objeto original queda inutilizable después de la transferencia.
Advertencia de transferencia
Cuando transfieres un objeto, pierdes el acceso a él en el contexto original. Si necesitas mantener el objeto en ambos contextos, usa la copia normal (sin el segundo parámetro) pero considera el impacto en rendimiento para objetos muy grandes.
Importar Scripts en Workers
Los Web Workers no tienen acceso a las etiquetas script del documento HTML, pero pueden cargar librerías externas usando la función global importScripts(). Esta función carga y ejecuta uno o más scripts de forma síncrona en el contexto del worker, permitiendo usar librerías de terceros.
La función importScripts() es síncrona y bloquea la ejecución del worker hasta que todos los scripts se carguen. Puedes cargar múltiples scripts en una sola llamada, y se ejecutarán en el orden especificado. Esto es útil para cargar librerías como lodash, moment.js o cualquier otra utilidad que necesites en tu worker.
CORS y importScripts
Los scripts importados deben cumplir con las políticas de CORS. Si intentas cargar un script desde otro dominio sin los headers CORS apropiados, obtendrás un error de seguridad. Para scripts externos, asegúrate de que el servidor permita el acceso cross-origin o descarga las librerías a tu servidor.
Terminar un Worker
Los Web Workers consumen recursos del sistema y deben ser terminados explícitamente cuando ya no se necesitan. Hay dos formas de terminar un worker: desde el hilo principal usando el método terminate(), o desde el propio worker usando self.close().
Terminar desde el Hilo Principal
El método terminate() detiene inmediatamente el worker sin dar oportunidad de limpieza. Es útil cuando necesitas detener el worker rápidamente o cuando el hilo principal detecta que el worker ya no es necesario.
El método terminate() es inmediato y no espera a que el worker termine su tarea actual. Esto puede dejar recursos sin liberar si el worker estaba en medio de una operación, por lo que úsalo con cuidado y considera implementar un mecanismo de cancelación limpio en tu lógica de negocio.
Cerrar desde el Worker
El método self.close() permite que el worker termine su propia ejecución de forma controlada. Esto es útil cuando el worker completa su tarea y sabe que ya no será necesario, o cuando detecta una condición que hace que deba detenerse.
Usar self.close() desde el worker es más elegante porque permite que el worker realice cualquier limpieza necesaria antes de terminar. El hilo principal recibirá el último mensaje antes de que el worker se cierre, lo que permite una coordinación más limpia.
Manejo de Errores
Los errores en los Web Workers no se propagan al hilo principal por defecto. Debes escuchar el evento error para capturar y manejar cualquier excepción que ocurra en el worker.
El evento error proporciona información detallada sobre el error, incluyendo el mensaje, el nombre del archivo y la línea donde ocurrió. Esta información es invaluable para debugging y para mostrar mensajes de error apropiados al usuario.
Debugging de Workers
Para depurar Web Workers en Chrome y Firefox, abre las DevTools, ve a la pestaña Sources y busca la sección "Workers". Allí encontrarás los archivos de los workers que puedes inspeccionar y depurar como cualquier otro script de JavaScript.
Resumen: Web Workers
Conceptos principales:
- •Los Web Workers ejecutan JavaScript en hilos separados del hilo principal
- •Se comunican mediante el sistema de mensajes usando postMessage y onmessage
- •No pueden acceder al DOM, window, document ni objetos del hilo principal
- •Los datos se copian por defecto, pero pueden transferirse para mejorar rendimiento
- •Usa importScripts() para cargar librerías externas dentro del worker
Mejores prácticas:
- •Usa workers para tareas intensivas de CPU como procesamiento de datos o cálculos
- •Termina explícitamente los workers con terminate() o self.close() para liberar recursos
- •Siempre maneja el evento error para capturar excepciones en el worker
- •Usa transferencia de objetos para datos grandes como ArrayBuffer o imágenes
- •Evita usar workers para tareas simples que tardan menos de 10ms