JavaScript — Web Workers API

Mauricio Garcia
21 min readSep 27, 2024

--

Introducción

Antes de comenzar, te recomiendo leer las siguientes stories para comprender el alcance y las ventajas del uso de workers en JavaScript:

  1. JavaScript — Cómo funciona el Runtime Environment (JRE) [ref]: Entiende cómo el entorno de ejecución de JS gestiona la ejecución de código, el event loop y las tareas asincrónicas.
  2. JavaScript — Callbacks y Promises [ref]: Aprende sobre la programación asincrónica en JS, utilizando callbacks y Promises para gestionar tareas que no se ejecutan secuencialmente.
  3. JavaScript — ES8 (ECMA2017) — Parte III (async/await) [ref]: Aprende cómo el uso de async/await ha simplificado el manejo de Promises y cómo puedes escribir código asincrónico de manera más legible y estructurada.
  4. Secuencia, concurrencia y paralelismo [ref]: Comprende los conceptos clave de secuencia, concurrencia y paralelismo, y cómo afectan la ejecución de tareas en entornos multihilo.

Ejemplo de generación de un conjunto de números aleatorios y análisis estadístico

Antes de explorar los Web Workers, realicemos un ejemplo sin utilizar Web Workers.

En este ejemplo, vamos a generar un número aleatorio entre 1 y 100,000,000. A partir de ese número, generamos esa misma cantidad de números aleatorios, los almacenaremos en un arreglo y realizaremos varios cálculos sobre ese conjunto de datos.

Utilizaremos la extensión Live Server de Visual Studio Code [ref], que nos permite levantar y bajar un servidor con un solo clic.

Los pasos son los siguientes:

  • Generación del número base:
    Generamos un número aleatorio entre 1 y 100,000,000. Este número determinará cuántos números aleatorios adicionales vamos a generar.
// Función para generar números aleatorios entre min y max
function generateRandomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// Generar un número aleatorio entre 1 y 100,000,000
const quantityNumbers = generateRandomNumber(1, 100000000);
  • Generación de números aleatorios:
    Crearemos un arreglo con la cantidad de números generados en el paso anterior. Cada uno de estos números será un valor aleatorio entre 1 y quantityNumbers.
// Crear un arreglo con esa cantidad de números aleatorios entre 1 y quantityNumbers
const dataNumAleatories = Array.from({ length: quantityNumbers }, () => generateRandomNumber(1, quantityNumbers));
  • Cálculos:
    Suma total: Sumaremos todos los números del arreglo.
    Promedio: Calcularemos el promedio dividiendo la suma total entre la cantidad de números generados.
    Desviación estándar: Calcularemos la desviación estándar, que nos indica la dispersión de los números alrededor de la media.
    Mínimo y máximo: Identificaremos los valores mínimo y máximo dentro del conjunto de números.
// Suma total
const sumTotal = dataNumAleatories.reduce((acc, num) => acc + num, 0);

// Calcular el promedio
const average = sumTotal / quantityNumbers;

// Calcular la desviación estándar
const deviationStandard = Math.sqrt(dataNumAleatories.reduce((acc, num) => acc + Math.pow(num - average, 2), 0) / quantityNumbers);

// Encontrar el mínimo y máximo
let min = datos[0];
let max = datos[0];
dataNumAleatories.forEach((num) => {
if (num < min) {
min = num;
}
if (num > max) {
max = num;
}
});
  • Medición del tiempo de ejecución:
    Usaremos performance.now() para medir el tiempo que tarda todo el proceso, desde la generación de los números hasta el cálculo de los resultados.
//-- Se pone antes de generar el número aleatorio
// Medir el tiempo de ejecución
const startTime = performance.now();

//-- Se pone al final de calcular min y max
// Medir el tiempo de ejecución
const endTime = performance.now();
const timeExecution = (endTime - startTime) / 1000;
  • Presentación de resultados:
    Mostraremos en pantalla con formato la suma total, el promedio, la desviación estándar, el valor mínimo, el valor máximo y el tiempo de ejecución en segundos.
const results = {
timeExecution: timeExecution.toFixed(2),
quantityNumbers: new Intl.NumberFormat().format(quantityNumbers),
sumTotal: new Intl.NumberFormat().format(sumTotal),
average: new Intl.NumberFormat().format(average.toFixed(2)),
deviationStandard: new Intl.NumberFormat().format(
deviationStandard.toFixed(2)
),
min: new Intl.NumberFormat().format(min),
max: new Intl.NumberFormat().format(max),
};

//Pesentamos resultados
console.log(results);

Primero realizaremos el procesamiento directamente en el archivo principal HTML y main.js. Más adelante, moveremos el procesamiento a un Web Worker para demostrar las bondades de usar workers en tareas intensivas.

Estructura del proyecto:

/project-root
├── index.html
└── main.js

En el archivo index.html se cargará el código en main.js.

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Generación de Números Aleatorios y Estadísticas (Sin Web Worker)</title>
</head>
<body>
<h1>Resultados (Sin Web Worker)</h1>
<div id="incrementalNumber"></div>
<p id="send"></p>
<p id="result"></p>
<script src="main.js"></script>
</body>
</html>

En el archivo main.js realizaremos todas las operaciones. Como estamos ejecutando tareas intensivas (generar muchos números aleatorios y hacer cálculos), es probable que experimentes bloqueos en la interfaz.

// Función para generar números aleatorios entre min y max
function generateRandomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

function generateRandomdata(quantityNumbers) {
// Medir el tiempo de ejecución
const startTime = performance.now();

// Pintamos los números que se van a generar
document.getElementById('send').innerText = `
Se esta generando con: ${new Intl.NumberFormat().format(
quantityNumbers
)} números aleatorios
--------------------------------------------------
`;

// Crear un arreglo con esa cantidad de números aleatorios entre 1 y quantityNumbers
const dataNumAleatories = Array.from({ length: quantityNumbers }, () =>
generateRandomNumber(1, quantityNumbers)
);

// Suma total
const sumTotal = dataNumAleatories.reduce((acc, num) => acc + num, 0);

// Calcular el promedio
const average = sumTotal / quantityNumbers;

// Calcular la desviación estándar
const deviationStandard = Math.sqrt(
dataNumAleatories.reduce(
(acc, num) => acc + Math.pow(num - average, 2),
0
) / quantityNumbers
);

// Encontrar el mínimo y máximo
let min = dataNumAleatories[0];
let max = dataNumAleatories[0];
dataNumAleatories.forEach((num) => {
if (num < min) {
min = num;
}
if (num > max) {
max = num;
}
});

// Medir el tiempo de ejecución
const endTime = performance.now();
const timeExecution = (endTime - startTime) / 1000;

//Guardamos los resultados y les damos formato
const results = {
timeExecution: timeExecution.toFixed(2),
quantityNumbers: new Intl.NumberFormat().format(quantityNumbers),
sumTotal: new Intl.NumberFormat().format(sumTotal),
average: new Intl.NumberFormat().format(average.toFixed(2)),
deviationStandard: new Intl.NumberFormat().format(
deviationStandard.toFixed(2)
),
min: new Intl.NumberFormat().format(min),
max: new Intl.NumberFormat().format(max),
};

//Pintamos el resultado en el navegador (lo cambiamos por la consola)
document.getElementById('result').innerText = `
--------------------------------------------------
Tiempo en generarse: ${results.timeExecution} segundos
Se generaron: ${results.quantityNumbers}
Total de la suma de los numeros random: ${results.sumTotal}
Promedio: ${results.average}
Desviación Estándar: ${results.deviationStandard}
Número Mínimo Random: ${results.min}
Número Máximo Random: ${max}}
${document.getElementById('result').innerText}
`;
}

// Generar un número aleatorio entre 1 y 100,000,000
// Ejecutamos calculo
generateRandomdata(generateRandomNumber(1, 100000000));

Si estás utilizando la extensión Live Server de Visual Studio Code, simplemente abre el archivo index.html y haz clic en "Go Live" en la parte inferior derecha para iniciar el servidor.

Resultados esperados:

  • El procesamiento de los números y los cálculos estadísticos se realiza directamente en el hilo principal. Es probable que notes un retraso o bloqueo en la interfaz mientras los cálculos se ejecutan, especialmente si el número generado es cercano a 100,000,000.

Hagamos un pequeño cambio...

Cuando se haya ejecutado correctamente todo, vamos a hacer que se genere un nuevo número aleatorio y que se repitan los cálculos nuevamente.

  //...
document.getElementById('result').innerText = `
--------------------------------------------------
Tiempo en generarse: ${results.timeExecution} segundos
Se generaron: ${results.quantityNumbers}
Total de la suma de los numeros random: ${results.sumTotal}
Promedio: ${results.average}
Desviación Estándar: ${results.deviationStandard}
Número Mínimo Random: ${results.min}
Número Máximo Random: ${max}}
${document.getElementById('result').innerText}
`;

//Pide de nuevo que haga otro calculo
generateRandomdata(generateRandomNumber(1, 100000000));
}
// ...

Revisamos el servidor:

Resultados esperados:

El navegador se queda en blanco y eventualmente se rompe debido a un bucle infinito que hemos creado dentro de la función generateRandomdata.

Esto significa que después de que se genera y procesa el primer conjunto de números aleatorios, inmediatamente se llama de nuevo a la función para generar otro conjunto de números aleatorios, y así sucesivamente, por lo que al navegador no le da tiempo de agregar los datos en pantalla, lo que lleva a que se congele y eventualmente se rompa.

Vamos a introducir una operación setTimeOut que espere 2 segundos para que al navegador le de tiempo de agregar los datos a la pantalla:

//Pide de nuevo que haga otro calculo y se agrega una espera de 2seg.
setTimeout(()=> generateRandomdata(generateRandomNumber(1, 100000000)), 2000);

Revisamos el servidor:

Resultados esperados:

El problema que seguimos visualizando, incluso después de usar setTimeout, es que la generación de una gran cantidad de números aleatorios y los cálculos asociados aún se realizan en el hilo principal. Esto está bloqueando la interfaz de usuario, por lo tanto, cuando el hilo principal está ocupado procesando esas tareas intensivas, la página no puede responder a las interacciones del usuario.

Para demostrar más claramente cómo el navegador se bloquea durante el procesamiento intensivo, incluso cuando los datos ya se están pintando, vamos a agregar un contador a la página.

Este contador funciona independientemente de los cálculos, y podrás observar que, cuando el navegador está bloqueado, el contador deja de actualizarse correctamente.

Código completo del index.html

<!DOCTYPE html>
<html lang="es">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Generación de Números Aleatorios y Estadísticas (Sin Web Worker)</title>
</head>

<body>
<h1>Resultados (Sin Web Worker)</h1>
<div id="incrementalNumber"></div>

<p id="send"></p>
<p id="result"></p>

<script src="main.js"></script>
</body>

</html>

Código completo del app.js

// Función para generar números aleatorios entre min y max
function generateRandomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

function generateRandomdata(quantityNumbers) {
// Medir el tiempo de ejecución
const startTime = performance.now();

// Pintamos los números que se van a generar
document.getElementById('send').innerText = `
Se esta generando con: ${new Intl.NumberFormat().format(
quantityNumbers
)} números aleatorios
--------------------------------------------------
`;

// Crear un arreglo con esa cantidad de números aleatorios entre 1 y quantityNumbers
const dataNumAleatories = Array.from({ length: quantityNumbers }, () =>
generateRandomNumber(1, quantityNumbers)
);

// Suma total
const sumTotal = dataNumAleatories.reduce((acc, num) => acc + num, 0);

// Calcular el promedio
const average = sumTotal / quantityNumbers;

// Calcular la media cuadrática
const halfQuadratic = Math.sqrt(
dataNumAleatories.reduce((acc, num) => acc + Math.pow(num, 2), 0) /
quantityNumbers
);

// Calcular la desviación estándar
const deviationStandard = Math.sqrt(
dataNumAleatories.reduce(
(acc, num) => acc + Math.pow(num - average, 2),
0
) / quantityNumbers
);

// Encontrar el mínimo y máximo
let min = dataNumAleatories[0];
let max = dataNumAleatories[0];
dataNumAleatories.forEach((num) => {
if (num < min) {
min = num;
}
if (num > max) {
max = num;
}
});

//Se pone al final de calcular min y max
// Medir el tiempo de ejecución
const endTime = performance.now();
const timeExecution = (endTime - startTime) / 1000;

//Guardamos los resultados y les damos formato
const results = {
timeExecution: timeExecution.toFixed(2),
quantityNumbers: new Intl.NumberFormat().format(quantityNumbers),
sumTotal: new Intl.NumberFormat().format(sumTotal),
average: new Intl.NumberFormat().format(average.toFixed(2)),
deviationStandard: new Intl.NumberFormat().format(
deviationStandard.toFixed(2)
),
min: new Intl.NumberFormat().format(min),
max: new Intl.NumberFormat().format(max),
};

document.getElementById('result').innerText = `
--------------------------------------------------
Tiempo en generarse: ${results.timeExecution} segundos
Se generaron: ${results.quantityNumbers}
Total de la suma de los numeros random: ${results.sumTotal}
Promedio: ${results.average}
Desviación Estándar: ${results.deviationStandard}
Número Mínimo Random: ${results.min}
Número Máximo Random: ${max}}
${document.getElementById('result').innerText}
`;

//Pide de nuevo que haga otro calculo
setTimeout(
() => generateRandomdata(generateRandomNumber(1, 100000000)),
2000
);
}

//Pintamos un número incremental cada 1segundo
let incrementalNumber = 0;
setInterval(
() =>
(document.getElementById('incrementalNumber').innerText =
++incrementalNumber),
1000
);

// Generar un número aleatorio entre 1 y 100,000,000
// ejecutamos calculo
generateRandomdata(generateRandomNumber(1, 100000000));

Web Workers

:: ¿Por qué necesitamos Web Workers?

Sabemos que JavaScript es un lenguaje cuya naturaleza es de un solo hilo (Single-Threaded), lo que significa que solo puede procesar una tarea a la vez. Esto incluye tanto la manipulación del DOM como la ejecución de cálculos complejos o el procesamiento de grandes volúmenes de datos.

Actualmente, si una tarea toma demasiado tiempo en ejecutarse, el hilo principal queda bloqueado, lo que impide que la interfaz de usuario responda a las interacciones. Esto provoca una serie de problemas que afectan directamente la experiencia del usuario, tales como:

  • Interfaz de usuario congelada: El navegador se bloquea y no responde a eventos como clics o desplazamientos, creando una experiencia frustrante para el usuario.
  • Mala experiencia de usuario: La falta de respuesta y la lentitud general de la aplicación pueden desesperar a los usuarios, resultando en una mala experiencia y la posible pérdida de engagement.

Todas las tareas, tanto ligeras como pesadas, compiten por el mismo hilo, lo que puede afectar negativamente al rendimiento de la aplicación si no se maneja adecuadamente.

:: ¿Cómo resuelven este problema los Web Workers?

Los Web Workers permiten la creación de programación paralela, lo que significa que puedes ejecutar tareas en un hilo separado. Esto resulta clave cuando se trata de operaciones pesadas como cálculos complejos, procesamiento de grandes volúmenes de datos. Los beneficios son más que claros:

  • Paralelismo: Los Web Workers permiten el procesamiento paralelo, sin afectar al rendimiento de la aplicación principal.
  • Mejor experiencia de usuario: Al evitar que el hilo principal se bloquee, la interfaz de usuario sigue siendo fluida y receptiva.
  • Optimización del rendimiento: Las tareas que normalmente llevarían mucho tiempo en el hilo principal, como la manipulación de grandes conjuntos de datos, cálculos matemáticos complejos o la renderización de gráficos, se pueden delegar a Web Workers, distribuyendo la carga de trabajo.

:: Limitantes de los Web Workers

A pesar de sus ventajas, los Web Workers tienen ciertas limitaciones que debemos tener en cuenta para su uso. Estas limitaciones se deben a que los workers se ejecutan en un entorno separado, llamado DedicatedWorkerGlobalScope[ref], y por eso tienen un alcance más restringido en comparación con el hilo principal de ejecución.

— Restricciones de los Web Workers

  • No tienen acceso al DOM: Los Web Workers no pueden manipular elementos HTML, como hacer cambios en el árbol DOM. Cualquier interacción con el DOM debe realizarse a través del hilo principal.
  • No pueden acceder a los objetos window, document o parent: Estos objetos están ligados al hilo principal de ejecución y no son accesibles desde el worker.
  • Contexto separado (DedicatedWorkerGlobalScope): El objeto this en un worker hace referencia a su propio contexto dedicado y no al objeto window del hilo principal.

— Métodos y Funcionalidades Disponibles en Web Workers

Aunque los Web Workers tienen restricciones, tienen acceso a varios métodos a través del mixin compartido WindowOrWorkerGlobalScope [ref].

Algunos de los métodos y funcionalidades disponibles en los workers son:

  • Comunicación entre hilos:
    postMessage(): Enviar datos al proceso principal.
    onmessage: Escuchar mensajes enviados desde el hilo principal.
  • Control del worker:
    close(): Finalizar el worker una vez que ha terminado su tarea.
  • Funciones de tiempo:
    setTimeout() y clearTimeout(): Ejecutar código después de un retraso específico.
    setInterval() y clearInterval(): Ejecutar código repetidamente en intervalos de tiempo.
  • Funciones globales de JavaScript:
    Métodos como eval(), isNaN(), parseInt(), entre otros, están disponibles dentro del entorno de un worker.
  • Peticiones de red y manejo asincrónico:
    Hacer peticiones y manejo de respuesta con Fetch API. Así como el uso de Promises para manejar programación asíncrona.

Para obtener una lista completa de los métodos y funcionalidades que están disponibles en los Web Workers, te recomiendo consultar la documentación oficial en Mozilla [ref][ref].

Web Workers API

Teniendo en cuenta las limitaciones de los Web Workers, veamos cómo funciona la comunicación entre el proceso principal y un worker. El flujo básico es el siguiente:

  1. El proceso principal crea una conexión con el subproceso worker.
  2. El proceso principal envía un mensaje al worker.
  3. El worker escucha el mensaje enviado desde el proceso principal.
  4. El worker procesa la información y envía (responde) una respuesta al proceso principal.
  5. El proceso principal recibe la respuesta enviada por el worker.

Ahora veamos cada uno de estos pasos más detalladamente:

:: Conectar el Proceso Principal con el Subproceso Worker

Para conectar el proceso principal con un subproceso worker, utilizamos el constructor Worker()[ref] :

//app.js
// Se conecta el proceso principal a un subproceso worker
const myWorker = new Worker('worker.js');

Este constructor crea un nuevo worker y carga el archivo JS especificado (en este caso, worker.js). Si el archivo existe, el navegador generará un nuevo subproceso donde JS se ejecutará de manera asíncrona. Es importante notar que en este paso el worker NO se ejecuta; solo se carga.

:: Comunicar un Mensaje desde el Proceso Principal al Subproceso Worker

Para enviar un mensaje desde el proceso principal al worker, utilizamos el método postMessage()[ref]:

//app.js
const myWorker = new Worker('worker.js');

//Se envía un mensaje desde el proceso principal
myWorker.postMessage('Hello Worker');

Este método envía un mensaje al worker de manera asincrónica. Recuerda que si no se proporciona el parámetro message, el navegador lanzará un SyntaxError.

El mensaje puede ser cualquier valor u objeto JS. Los valores se manejan por el algoritmo de clonación estructurado (incluyendo referencias cíclicas)

El algoritmo de clonación estructurado crea una copia profunda del objeto que se está enviando, lo que significa que no se comparte la referencia original entre el proceso principal y el worker, sino una copia de los datos.

:: Escuchar el Mensaje desde el Subproceso Worker

En el worker, podemos escuchar el mensaje enviado desde el proceso principal utilizando el evento message[ref]:

//worker.js
//Escuchamos el mensaje enviado
addEventListener('message', (evt) => {
console.log('Mensaje recibido del proceso principal:', evt.data);
});

También se puede mediante una función de flecha [ref]:

//worker.js
//Escuchamos el mensaje enviado
onmessage = (evt) => {
console.log('Mensaje recibido del proceso principal:', evt.data);
});

En ambos ejemplos, el worker está a la espera de recibir un mensaje desde el proceso principal. Cuando se recibe el mensaje, podemos acceder a él mediante evt.data. Es importante notar que el contexto de this en un worker no hace referencia al objeto window, sino al propio worker.

:: Responder un Mensaje desde el Worker al Proceso Principal

Para que el worker envíe una respuesta de vuelta al proceso principal, también usamos el método postMessage()[ref]:

//worker.js
addEventListener('message', (evt) => {
console.log('Mensaje recibido del proceso principal:', evt.data);

//Respondemos un mensaje desde el subrpoceso worker
postMessage('Hola desde el worker');
});

En este caso, el worker envía un mensaje de vuelta. De nuevo, el contexto de this dentro del worker se refiere a él mismo, no a window. El mensaje puede ser cualquier valor u objeto JS. Los valores se manejan por el algoritmo de clonación estructurado (incluyendo referencias cíclicas)

:: Escuchar el Mensaje desde el Proceso Principal

El proceso principal puede escuchar los mensajes enviados por el worker utilizando el evento message[ref]:

//app.js
const myWorker = new Worker('worker.js');

myWorker.postMessage('Hello Worker');

// Escuchamos el mensaje desde el proceso principal
myWorker.addEventListener('message', (evt) => {
console.log('Mensaje recibido del worker:', evt.data);
});

Este código captura la respuesta enviada por el worker y la procesa en el hilo principal.

Ejemplos

Ahora que comprendemos la sintaxis básica de los Web Workers, vamos a ver varios ejemplos. Para poder ejecutar Web Workers, es necesario contar con un servidor, ya que los workers no funcionan cuando se ejecutan directamente desde el sistema de archivos.

Primero, creamos la siguiente estructura de archivos:

/project-root
├── index.html
├── main.js
└── worker.js

Donde:

  • index.html: Se encargará de cargar el archivo JS principal (main.js).
  • main.js: Archivo que pertenece al proceso principal y será el encargado de instanciar y comunicarse con el worker (worker.js).
  • worker.js: Archivo que se ejecutará como subproceso (worker).

:: Ejemplo básico

Dentro del archivo index.html, vamos a crear la estructura base de un documento HTML5:

<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Worker Basic Example</title>
</head>
<body>
<div id="app"></div>
<script src="main.js"></script>
</body>
</html>

El archivo main.js se encargará de validar si el navegador soporta Web Workers y de instanciar el worker. También se encargará de enviar mensajes al worker y recibir respuestas.

// Validar si el navegador soporta Web Workers
if (window.Worker) {
// Instanciamos un nuevo Worker
const myWorker = new Worker('worker.js');

// Enviar mensaje al worker
myWorker.postMessage('Mauricio');

// Escuchar respuesta del worker
myWorker.addEventListener('message', (evt) => {
// Mostramos resultado en el navegador
console.log('Mensaje recibido del worker:', evt.data);
});
} else {
document.getElementById('app').innerText = 'Tu navegador no soporta Web Workers';
}

El archivo worker.js será el subproceso que recibe mensajes del proceso principal y envía respuestas.

// Escuchar mensaje del proceso principal
addEventListener('message', (evt) => {
// Recibe el mensaje enviado por el proceso principal
const name = evt.data;
// Imprime en la consola del worker
console.log(`Mensaje recibido: ${name} desde el proceso principal`);

// Responder al proceso principal
postMessage(`Hola ${name}, desde worker.js`);
});

Si estás utilizando la extensión Live Server de Visual Studio Code, simplemente abre el archivo index.html y haz clic en "Go Live" en la parte inferior derecha para iniciar el servidor.

Al abrir la consola del navegador, deberías ver el siguiente mensaje:

Esto muestra que el flujo de comunicación entre el proceso principal y el worker está funcionando correctamente.

:: Ejemplo con objetos

También podemos enviar objetos JS más complejos. Por ejemplo, en el archivo main.js, podemos cambiar el mensaje por un objeto:

if (window.Worker) {
const myWorker = new Worker('worker.js');

// Enviar mensaje al worker
myWorker.postMessage({
name: 'Mauricio',
profession: 'Developer Intern'
});

myWorker.addEventListener('message', (evt) => {
console.log('Mensaje recibido del worker:', evt.data);
});
} else {
document.getElementById('app').innerText = 'Tu navegador no soporta Web Workers';
}

En el archivo worker.js, podemos procesar este objeto y enviarlo de vuelta:

addEventListener('message', (evt) => {
// Recibe el mensaje enviado por el proceso principal
const { name, profession } = evt.data;

// Imprime en la consola del worker
console.log(`Mensaje recibido: ${name} ${profession} desde el proceso principal`);

postMessage(`Hola ${name}, desde worker.js`);
});

Al abrir la consola del navegador, deberías ver el siguiente mensaje:

:: Ejemplo para terminar o cerrar un web worker

Existen dos formas principales de cerrar o terminar un Web Worker:

  • Desde el proceso principal usando terminate()[ref].
  • Desde el mismo worker utilizando close()[ref].

— Cerrar el Worker desde el Proceso Principal (terminate())

Esta opción se utiliza para finalizar el Worker desde el hilo principal. El worker se detiene y ya no puede recibir ni enviar mensajes.

if (window.Worker) {
const myWorker = new Worker('worker.js');

myWorker.postMessage({
name: 'Mauricio',
profession: 'Developer Intern'
});

myWorker.addEventListener('message', (evt) => {
console.log('Mensaje recibido del worker:', evt.data);

// Cerrar el worker desde el proceso principal
myWorker.terminate();
console.log('Worker terminado desde el proceso principal.');
});
} else {
document.getElementById('app').innerText = 'Tu navegador no soporta Web Workers';
}

— Cerrar el Worker desde el Mismo Worker (close())

Esta opción se utiliza para finalizar el Worker desde dentro de sí mismo. El worker dejará de funcionar y ya no podrá comunicarse con el proceso principal.

addEventListener('message', (evt) => {
const { name, profession } = evt.data;

console.log(`Mensaje recibido: ${name} ${profession} desde el proceso principal`);

postMessage(`Hola ${name}, desde worker.js`);

// Cerrar el worker desde el mismo worker
close();
console.log('Worker cerrado desde dentro del worker.');
});

:: Ejemplo de la generación de un conjunto de números aleatorios y análisis estadístico con Web Workers

Con Web Workers, puedes delegar la tarea intensiva a un subproceso separado, mientras que el hilo principal sigue respondiendo.

Estructura del proyecto:

/project-root
├── index.html
├── main.js
└── worker.js

Aprovecharemos el primer ejemplo que hicimos. En el archivo index.html solo cambiamos los títulos:

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Generación de Números Aleatorios y Estadísticas (Con Web Worker)</title>
</head>

<body>
<h1>Resultados (Con Web Worker)</h1>
<div id="incrementalNumber"></div>
<p id="send"></p>
<p id="result"></p>

<script src="main.js"></script>
</body>
</html>

El archivo main.js se encargará de validar si el navegador soporta Web Workers y de instanciar el worker. También se encargará de enviar mensajes al worker y recibir respuestas.

if (window.Worker) {
const myWorker = new Worker('worker.js');
} else {
document.getElementById('result').innerText = 'Tu navegador no soporta Web Workers';
}

Dentro del código principal, movemos la función generateRandomNumber :

if (window.Worker) {
const myWorker = new Worker('worker.js');

// Función para generar números aleatorios entre min y max
function generateRandomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
} else {
document.getElementById('result').innerText = 'Tu navegador no soporta Web Workers';
}

Crearemos una función llamada generateRandomData que será responsable de generar un número aleatorio (la cantidad de números que vamos a procesar), y mandarlo al Worker para que realice los cálculos:

if (window.Worker) {
const myWorker = new Worker('worker.js');

function generateRandomNumber(min, max) {
// ...
}

// Función para generar número y mandarlo al worker
function generateRandomData() {
const quantityNumbers = generateRandomNumber(1, 100000000);

// Enviar al worker para realizar los cálculos
myWorker.postMessage(quantityNumbers);

// Mostrar la cantidad de números que se están generando
document.getElementById('send').innerText = `
Se está generando con: ${new Intl.NumberFormat().format(quantityNumbers)} números aleatorios
--------------------------------------------------
`;
}
} else {
document.getElementById('result').innerText = 'Tu navegador no soporta Web Workers';
}

Una vez que el Worker termine de realizar los cálculos, enviará los resultados de vuelta al hilo principal. El hilo principal escuchará estos resultados a través del evento onmessage:

if (window.Worker) {
const myWorker = new Worker('worker.js');

function generateRandomNumber(min, max) {
// ...
}

function generateRandomData() {
// ...
}

// Escuchar al worker para pintar resultados y volver a ejecutar
myWorker.onmessage = function (e) {
const {
timeExecution,
quantityNumbers,
sumTotal,
average,
deviationStandard,
min,
max,
} = e.data;

// Mostrar los resultados en pantalla
document.getElementById('result').innerText = `
--------------------------------------------------
Tiempo en generarse: ${timeExecution} segundos
Se generaron: ${quantityNumbers}
Total de la suma de los números random: ${sumTotal}
Promedio: ${average}
Desviación Estándar: ${deviationStandard}
Número Mínimo Random: ${min}
Número Máximo Random: ${max}
${document.getElementById('result').innerText}
`;

// Volver a generar nuevos números aleatorios (sin setTimeout)
generateRandomData();
};
} else {
document.getElementById('result').innerText = 'Tu navegador no soporta Web Workers';
}

Para demostrar que la interfaz de usuario no se bloquea mientras el Worker realiza cálculos en segundo plano, moveremos el contador que incrementa cada segundo:

if (window.Worker) {
const myWorker = new Worker('worker.js');

function generateRandomNumber(min, max) {
// ...
}

function generateRandomData() {
// ...
}

myWorker.onmessage = function (e) {
// ...
};

//Pintamos un número incremental cada 1segundo
let incrementalNumber = 0;
setInterval(() => {
document.getElementById('incrementalNumber').innerText = ++incrementalNumber;
}, 1000);
} else {
document.getElementById('result').innerText = 'Tu navegador no soporta Web Workers';
}

Finalmente, llamamos a la función generateRandomdata() para iniciar el ciclo. Cada vez que el Worker termine de procesar, volverá a generar otro conjunto de números y repetirá el ciclo:

if (window.Worker) {
const myWorker = new Worker('worker.js');

function generateRandomNumber(min, max) {
// ...
}

function generateRandomData() {
// ...
}

myWorker.onmessage = function (e) {
// ...
};

let incrementalNumber = 0;
setInterval(() => {
// ...
}, 1000);

generateRandomData();
} else {
document.getElementById('result').innerText = 'Tu navegador no soporta Web Workers';
}

El archivo worker.js será el subproceso que recibe mensajes del proceso principal y envía respuestas. Por lo que le delegamos toda la carga intensiva de los cálculos:

// Función para generar números aleatorios entre min y max
function generateRandomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

// Escuchar mensaje desde el proceso principal
addEventListener('message', (e) => {
// Medir el tiempo de ejecución
const startTime = performance.now();

//Obtenemos cuantos números aleatorios debemos generar
const quantityNumbers = e.data;

// Crear un arreglo con esa cantidad de números aleatorios entre 1 y quantityNumbers
const dataNumAleatories = Array.from({ length: quantityNumbers }, () =>
generateRandomNumber(1, quantityNumbers)
);

// Suma total
const sumTotal = dataNumAleatories.reduce((acc, num) => acc + num, 0);

// Calcular el promedio
const average = sumTotal / quantityNumbers;

// Calcular la desviación estándar
const deviationStandard = Math.sqrt(
dataNumAleatories.reduce(
(acc, num) => acc + Math.pow(num - average, 2),
0
) / quantityNumbers
);

// Encontrar el mínimo y máximo
let min = dataNumAleatories[0];
let max = dataNumAleatories[0];
dataNumAleatories.forEach((num) => {
if (num < min) {
min = num;
}
if (num > max) {
max = num;
}
});

// Medir el tiempo de ejecución
const endTime = performance.now();
const timeExecution = (endTime - startTime) / 1000;

//Guardamos los resultados y les damos formato
const results = {
timeExecution: timeExecution.toFixed(2),
quantityNumbers: new Intl.NumberFormat().format(quantityNumbers),
sumTotal: new Intl.NumberFormat().format(sumTotal),
average: new Intl.NumberFormat().format(average.toFixed(2)),
deviationStandard: new Intl.NumberFormat().format(
deviationStandard.toFixed(2)
),
min: new Intl.NumberFormat().format(min),
max: new Intl.NumberFormat().format(max),
};

// Enviar los resultados al proceso principal
postMessage(results);
});

Si estás utilizando la extensión Live Server de Visual Studio Code, simplemente abre el archivo index.html y haz clic en "Go Live" en la parte inferior derecha para iniciar el servidor.

Resultados esperados:

  • Generación y procesamiento de números: Los cálculos intensivos se delegan al Worker, permitiendo que el hilo principal mantenga la interfaz interactiva.
  • Actualización sin bloqueo: El contador demuestra que la interfaz sigue respondiendo incluso cuando se están realizando cálculos intensivos en segundo plano.
  • Ciclo continuo: Cada vez que se obtienen los resultados del Worker, el ciclo se repite y se generan nuevos números aleatorios, demostrando cómo los Web Workers permiten manejar cálculos intensivos sin afectar la interfaz de usuario.

— Ejemplo funcionando en Sandbox [ref]

— Descarga el código

Github[tag: 1.0.0][ref].

He actualizado el ejemplo práctico donde muestra cómo crear una gráfica que se actualiza en tiempo real.

Es un buen ejemplo de cómo manejar datos en movimiento, manteniendo la interfaz sin bloqueos gracias a Web Workers, lo que permite que la actualización sea fluida y no interfiera con la experiencia del usuario.

Pueden revisar el código y ver cómo se actualiza automáticamente. Este enfoque es ideal para quienes necesiten implementar gráficos interactivos en sus proyectos web.

— Ejemplo funcionando en Sandbox [ref]

— Descarga el código

Github[tag: 2.0.0][ref].

Conclusión

Los Web Workers son la solución efectiva cuando se necesitan evitar bloqueos, mejorar la experiencia de usuario, especialmente en tareas que demandan un procesamiento intensivo. Sin embargo, es muy importante utilizarlos correctamente, reconociendo sus limitaciones.

Aunque los Workers permiten paralelismo, aún comparten recursos como la CPU y la memoria. Crear demasiados Workers puede sobrecargar el sistema, lo que llevaría a una disminución del rendimiento.

--

--

No responses yet