HTML 5 Web Workers

Hacía mucho tiempo que quería escribir sobre una de las APIs más importantes dentro de la especificación de HTML 5, y no es otra que aquella que nos permite ejecutar hilos en segundo plano para los procesos de larga duración que podrían hacer que nuestro navegador se vuelva totalmente inestable.

Web Worker

No debemos olvidar que, cuando trabajamos en web, el hilo en el que se está ejecutando nuestra aplicación es el encargado de gestionar los eventos, cálculos, representar la interfaz, etcétera. Es por ello, que en numerosas ocasiones hemos sufrido el gran inconveniente que esto supone: si una de las acciones que se realiza en cliente tarda más tiempo del esperado, la pantalla queda congelada hasta que la acción finaliza o bien hasta que el navegador nos da la opción de finalizar la ejecución de este script por nosotros.

Antes de nada, pongámonos en situación:

Lo primero que voy a hacer es simular un proceso pesado, a través de esta función sleep:

        function sleep(milliseconds) {
            var start = new Date().getTime();
            for (;;) {
                if ((new Date().getTime() - start) > milliseconds) {
                    break;
                }
            }
        }

Para invocar al anterior, manejo el evento click de un botón y pongo mi hilo principal a “trabajar” durante 5 segundos, con el objetivo de que el mismo no pueda atender otras tareas:

        var btnSleep = document.getElementById('btnSleep');

        btnSleep.addEventListener("click", function () {
            console.log(new Date());
            sleep(5000);
            console.log(new Date());
        });

Parece un ejemplo muy simple pero lo suficientemente potente para hacer que nuestra aplicación quede inservible durante este tiempo:

pacman-gif-

En un caso real, este comportamiento puede ser desastroso para nosotros y es donde entran en juego los Web Workers de HTML 5.

Veamos qué ocurre cuando utilizamos un worker para gestionar nuestro proceso simulado:

pacman-gif-

En este segundo ejemplo, como se puede apreciar, si pulsamos el botón sleep la página no se congela y, además aparece el texto “Wait for it…” (How I Met Your Mother lover :P) indicando que algo está pasando. Una vez finaliza, nos muestra el mismo resultado que en el ejemplo anterior. Veamos cómo implementarlo:

    window.onload = function () {

        var btnSleep = document.getElementById('btnSleep');

        btnSleep.addEventListener("click", function () {

            var worker = new Worker("/Scripts/sleep-worker.js");

            worker.addEventListener("message", function (e) {
                message.innerHTML = e.data;
            });

            worker.addEventListener("error", function (e) {
                message.innerHTML = e.message;
            });

            worker.postMessage(5000);

        });
    };

En primer lugar, manejamos el evento del botón Sleep y, en este caso, creamos un objeto del tipo Worker, el cual recibe como parámetro la ruta del archivo JavaScript que vamos a ejecutar en segundo plano. Asignamos al objeto los eventos message, que nos indica que el hilo en segundo plano enviado un mensaje a su padre, y el evento error para lo obvio. Una vez definidos estos manejadores, lanzamos la petición al worker pasándole como parámetro 5000 milisegundos para que el comportamiento sea el mismo que en el ejemplo anterior.

¿Qué es lo que contiene el archivo sleep-worker.js?

function sleep(milliseconds) {
    var start = new Date().getTime();
    for (;;) {
        if ((new Date().getTime() - start) > milliseconds) {
            break;
        }
    }
}

addEventListener("message", function (e) {
    if (e.data > 0) {
        var start = new Date();
        sleep(e.data);
        var end = new Date();

        var result = 'Proccess completed:<br/> <strong>start</strong>:' + start + '<br/> <strong>end</strong>:' + end;

        postMessage(result);

    }
});

Además de la función sleep que conservamos del primer ejemplo, hemos añadido un event listener para el evento message, donde comprobamos que e.data sea mayor que cero y llamamos a la función sleep, capturando además los mismos valores que la vez anterior para devolverlo en forma de resultado, a través de postMessage al hilo principal. Una duda que podría aparecer en este punto es ¿Acaso no puedo actualizar directamente el DOM en lugar de tener que enviarle el resultado a través de un evento al hilo principal? Lo cierto es que no, y es que los Web Workers sólo tienen acceso a un subconjunto de funcionalidades de la página:

  • Importación de scripts y librerías a través de importScripts
  • Objeto navigator a través de la interfaz WorkerNavigator
  • XMLHttpRequest para realizar peticiones.
  • location sólo de lectura a través de WorkerLocation
  • Pueden crear otros workers
  • Acceso a Application Cache API
  • Creación de intervalos y time outs

Por otro lado, el acceso al DOM, objetos como window, document o parent no están disponibles desde los mismos.

Espero que haya sido de utilidad.

Web Workers W3C
CanIUse Web Workers

¡Saludos!