Web Worker en HTML5

  • Por
Vemos Web Worker en HTML5: ejecución de Javascript en multi-threading.
Las aplicaciones para HTML5 se escriben –como no podría ser de otra manera- en Javascript. Pero si se compara con otros entornos de desarrollo (por ejemplo nativos para un S.O.), Javascript adolece desde siempre de una limitación importante: todo su proceso de ejecución se mantiene siempre dentro de un único thread.

Esto puede parecernos simplemente insólito en un mundo como el actual con procesadores multi-core como el i5/i7 que llegan a ofrecer hasta 8 CPUs lógicas, e incluso con los últimos procesadores ARM para dispositivos móviles, que ya los hay con dos o incluso cuatro cores. Por suerte, estamos viendo que HTML5 propone a la Web una alternativa mejor para aprovechar estos nuevos e increíbles procesadores, lo que contribuirá sin duda a extender una nueva generación de aplicaciones Web.

Antes de los Web Workers…

Esta limitación de Javascript implica que un proceso de larga duración puede colgar toda la ventana. A menudo se dice que “bloquea el thread de la interfaz de usuario (UI)”. Este es el principal thread encargado de manejar todos los elementos visuales y sus tareas conexas: dibujo, refresco de pantalla, animación, eventos de entrada de información de usuario, etc.

Todos sabemos las penosas consecuencias que se derivan de la sobrecarga de este thread: la página se cuelga y el usuario ya no puede interactuar con su aplicación. La experiencia de usuario es, lógicamente, muy mala y el usuario probablemente decidirá cerrar la pestaña o la instancia del navegador. Sin duda, el tipo de actitudes que no quieres para tu aplicación.

Para evitarlo los navegadores han implementado un mecanismo de protección que avisa al usuario cuando entra en ejecución un script que sospechosamente captura recursos de máquina durante un periodo de tiempo excesivo.

Sin embargo, este mecanismo es incapaz de reconocer la diferencia entre un script mal hecho y uno que simplemente necesita más tiempo de lo normal para completar su actividad. Aun así, puesto que el thread de la UI se puede bloquear, es mejor avisar al usuario de que probablemente sucede algo anómalo. Estos son dos ejemplos de este tipo de mensajes (del Firefox 5 e IE9):

Hasta ahora, estos problemas no eran frecuentes, principalmente por dos motivos:

  1. HTML y JavaScript no se utilizaban de la misma manera y para los mismos objetivos que otras tecnologías capaces de distribuir sus tareas en múltiples hilos. Los sitios web en general ofrecen al usuario experiencias más pobres, comparadas con las de las aplicaciones nativas.
  2. Hay también otras formas de resolver (más o menos bien) este problema de concurrencia.
Estas alternativas son muy conocidas por todos los desarrolladores Web. Por ejemplo, tratábamos de simular la ejecución de tareas en paralelo utilizando los métodos setTimeout() y setInterval(). Las peticiones HTTP se pueden también ejecutar de manera asíncrona con la ayuda del objeto XMLHttpRequest que evita el cuelgue de la UI mientras se cargan recursos desde servidores remotos. Finalmente, los Eventos del DOM nos permiten programar aplicaciones dando la apariencia de que ocurren varias cosas al mismo tiempo. Ilusión, ¿realidad? ¡Pues sí!

Para entender mejor la razón, vamos a echar un vistazo a una especie de pseudocódigo en Javascript y veamos qué sucede dentro del navegador:

<script type="text/javascript">
    function init(){
      { parte del código que se ejecuta en 5ms }
      Se activa mouseClickEvent
      { parte del código que se ejecuta en 5ms }
      setInterval(timerTask,"10");
      { parte del código que se ejecuta en 5ms }
   }

function handleMouseClick(){
   parte del código que se ejecuta en 8ms
}

function timerTask(){
   parte del código que se ejecuta en 2ms
}
</script>

Vamos a proyectar este código dentro de un modelo. Este diagrama nos muestra qué sucede en el navegador en una escala de tiempo:

Este diagrama muestra claramente la naturaleza no paralela de nuestras tareas. Antes bien, el navegador se limita a encolar las distintas peticiones de ejecución:

  • Desde el milisegundo 0 al 5: la función init() arranca una tarea que tarda unos 5 ms. en completarse. Tras los 5 ms. el usuario activa un evento de pulsación de ratón. Sin embargo, el evento no se puede manejar en ese momento puesto que aún se sigue ejecutando la función init() que en este momento monopoliza el thread principal. El evento de click se guarda y se manejará en un momento posterior.
  • Desde el milisegundo 5 al 10: la función init() sigue su procesamiento durante 5 ms. y después pide planificar la llamada al timerTask() en 10ms. Esta función debería, por lógica, ejecutarse en el punto 20 ms. de la línea de tiempo.
  • Desde el milisegundo 10 al 15: se necesitan 5 milisegundos más para terminar la ejecución de la función init(). Este es, por tanto el intervalo que corresponde al bloque de 15 ms amarillo. En cuanto se libera el thread principal, puede empezar a desencolar las peticiones guardadas.
  • Desde el milisegundo 15 al 23: el navegador empieza a ejecutar el evento handleMouseClock() que se ejecuta durante 8 ms. (el bloque azul).
  • Desde el milisegundo 23 al 25: como efecto colateral, la función timerTask() que estaba prevista para ejecutarse en el milisegundo 20 de la línea de tiempo, se desplaza unos 3 ms. Los otros puntos planificados (30 ms., 40 ms. etc.) se respetan ya que no hay más código capturando recursos de CPU.
Nota: Este ejemplo y el diagrama (en SVG o PNG, depende del mecanismo de detección de funcionalidad) se ha inspirado en el artículo HTML5 Web Workers Multithreading in JavaScript

Todas estas puntualizaciones realmente no resuelven el problema de partida: todo se ejecuta dentro del thread principal de la UI.

Pero sucede que, a pesar de que Javascript no se ha venido utilizando para el mismo tipo de aplicaciones que los “lenguajes de alto nivel”, esto empieza a cambiar merced a las nuevas posibilidades que introducen HTML5 y sus amigos. Es, por tanto, más importante que nunca dotar al Javascript de más potencia para que pueda permitirnos crear una nueva generación de aplicaciones con capacidad de procesamiento en paralelo. Esto es para lo que han nacido precisamente los Web Workers

Los Web Workers o cómo puede ejecutarse algo fuera del thread de la Interfaz de Usuario

El API Web Workers define una manera de ejecutar scripts en segundo plano. Podemos, bajo esta especificación, ejecutar algunas tareas en threads que residen fuera de la página principal y que por tanto, no afectan al rendimiento de la tarea de restitución en pantalla. De todas formas, igual que sabemos que no todos los algoritmos se pueden ejecutar en paralelo, no todo código Javascript puede aprovechar la ventaja que suponen los Workers. OK, vale de cháchara, vamos a conocer más de cerca quienes son estos famosos Web Workers.

Mi primer Web Worker

Puesto que los Web Workers se ejecutan en threads independientes, necesitamos colocar su código en archivos independientes de la página principal. Después tendremos que instanciar un objeto Worker para hacerle la llamada:

var myHelloWorker = new Worker('helloworkers.js');

Luego podemos inicializar el worker (y con él un thread en Windows), enviándole un primer mensaje:

myHelloWorker.postMessage();

Obviamente, los workers y la página principal se comunican mediante mensajes. Estos mensajes se pueden crear con cadenas normales u objetos JSON. Como ejemplo de un intercambio sencillo de mensajes, vamos a empezar por revisar un ejemplo muy básico. Publicará una cadena de texto a un worker que simplemente se limitará a concatenarla con cualquier otra. Para ello añadimos el siguiente código dentro del archivo “helloworker.js” file:

function messageHandler(event) {
   // Accede a los datos del mensaje enviado por la página principal
   var messageSent = event.data;
   // Prepara el mensaje que se va a devolver
   var messageReturned = "¡Hola " + messageSent + " desde un thread distinto!";
   // Publica el mensaje de vuelta en la página principal
   this.postMessage(messageReturned);
}

// Declara la function de callback que se ejecutará cuando la página principal nos haga una llamada
this.addEventListener('message', messageHandler, false);

Simplemente hemos definido dentro de “helloworkers.js” un pequeño código que se ejecutará en otro thread. Puede recibir mensajes de nuestra página principal, hacer con ellos alguna cosa y retornar otro mensaje de vuelta a la página principal. Después tenemos que escribir el código receptor en la página principal. Este es el código completo con el manejador del mensaje de retorno:

<!DOCTYPE html>
<html>
<head>
    <title>Hola Obreros de la Web</title>
</head>
<body>
    <div id="output"></div>

    <script type="text/javascript">
       // Instancia el Worker
       var myHelloWorker = new Worker('helloworkers.js');
       // Se prepara para manejar el mensaje que devuelve
       // el worker
       myHelloWorker.addEventListener("message", function (event) {
          document.getElementById("output").textContent = event.data;
       }, false);

    // Inicializa el worker enviándole un prmer mensaje
    myHelloWorker.postMessage("David");

    // Detiene el worker con el comando terminate()
    myHelloWorker.terminate();
    </script>
</body>
</html>

El resultado será: “Hola David desde un thread distinto!”. Impresionante ¡Eh!

Ten en cuenta que el obrero va a estar vivo hasta que lo detengamos mediante comando.

Puesto que no hay ningún sistema de eliminación de basura, nos toca controlar el estado de los workers. Y debemos tener presente que instanciar un worker consume memoria… y tampoco hay que olvidar el tiempo que requiere para su arranque inicial. Para detener un worker tenemos dos alternativas:

  1. Desde la página principal llamando al comando terminate()
  2. Desde el propio worker utilizando el comando close().
DEMO: Puedes ver este mismo ejemplo con algunas mejoras en tu propio navegador, en: http://david.blob.core.windows.net/html5/HelloWebWorkers_EN.htm

Publicar mensajes utilizando JSON

Obviamente, en la mayoría de ocasiones los workers se intercambiarán datos más estructurados (y por cierto, los Web Workers pueden comunicarse unos con otros utilizando los canales de mensaje.)

Pero la única manera de enviar mensajes estructurados a un worker es mediante el uso de formato JSON. Por suerte, los navegadores que soportan actualmente los Web Workers son suficientemente avanzados como para soportar JSON de forma nativa. ¡Qué maravillosos!

Volvamos a nuestro ejemplo anterior. Vamos a añadir un objeto del tipo WorkerMessage. Este tipo se utilizará para enviar algunos comandos con parámetros a nuestros “obreros”.

Vamos a utilizar una versión simplificada de la página web HelloWebWorkersJSON_EN.htm:

<!DOCTYPE html>
<html>
<head>
    <title>Hola Web Workers JSON</title>
</head>
<body>
   <input id=inputForWorker /><button id=btnSubmit>Enviar al worker</button><button id=killWorker>Detener el worker</button>
    <div id="output"></div>

    <script src="HelloWebWorkersJSON.js" type="text/javascript"></script>
</body>
</html>
Utilizamos la estrategia no obstructiva de Javascript que nos ayuda a disociar la parte visual de la lógica asociada. La lógica asociada reside en el archivo HelloWebWorkersJSON_EN.js cuyo código es:
// HelloWebWorkersJSON_EN.js asociado a HelloWebWorkersJSON_EN.htm

// Nuestro objeto WorkerMessage se serializará y
// de-serializará automáticamente en el analizador JSON nativo
function WorkerMessage(cmd, parameter) {
    this.cmd = cmd;
   this.parameter = parameter;
}

// DIV de salida donde se mostrarán los mensajes devueltos por el worker
var _output = document.getElementById("output");

/* Comprueba si el navegador soporta Web Workers */
if (window.Worker) {
    // Obtiene la referencia de los otros 3 elementos HTML
    var _btnSubmit = document.getElementById("btnSubmit");
    var _inputForWorker = document.getElementById("inputForWorker");
    var _killWorker = document.getElementById("killWorker");

   // Instancia el Worker
    var myHelloWorker = new Worker('helloworkersJSON_EN.js');
    // Se prepara para manejar el mensaje devuelto
    // por el worker
   myHelloWorker.addEventListener("message", function (event) {
       _output.textContent = event.data;
   }, false);

    // Arranca el worker con el comando 'init'
    myHelloWorker.postMessage(new WorkerMessage('init', null));

    // Añade el evento OnClick al botón Submit
    // que enviará algunos mensajes al worker
    _btnSubmit.addEventListener("click", function (event) {
       // Ya estamos enviando mensajes por medio del comando 'hello'
       myHelloWorker.postMessage(new WorkerMessage('hello', _inputForWorker.value));
    }, false);

    // Añade el evento OnClick al botón Kill
    // que debe parar el worker. Ya no se podrá utilizar más después de eso.
    _killWorker.addEventListener("click", function (event) {
       // Para el worker mediante el comando terminate()
       myHelloWorker.terminate();
       _output.textContent = "El worker se ha parado.";
    }, false);
}
else {
    _output.innerHTML = "El navegador no soporta Web Workers. Prueba con IE10: <a href="http://ie.microsoft.com/testdrive">descarga el ultimo Platform Preview de IE10</a>";
}

Al final, el código para el Web Worker que debe tener el archivo helloworkerJSON_EN.js es este:

function messageHandler(event) {
    // Accede al mensaje enviado por la página principal
    var messageSent = event.data;

    // Comprueba el comando enviado por la página principal
    switch (messageSent.cmd) {
       case 'init':
          // Puedes inicializar aquí algunos de tus modelos/objetos
          // que luego puedes utilizar en el worker (pero ten en cuenta su ámbito de uso!)
       break;
       case 'hello':
          // Prepara el mensaje que va a devolver
          var messageReturned = "Hola " + messageSent.parameter + " desde un thread distinto!";
          // Devuelve el mensaje a la página principal
          this.postMessage(messageReturned);
          break;
       }
}

// Define la función de callback que se activará cuando la página principal nos llame
this.addEventListener('message', messageHandler, false);

Como ves, el ejemplo es muy básico, aunque nos ayuda a entender la lógica subyacente. por ejemplo, nada nos impide utilizar esta misma estrategia para enviar algunos elementos de un juego que puedan manejarse desde un motor de inteligencia artificial o de física.

DEMO: Puedes probar este ejemplo de JSON aquí: http://david.blob.core.windows.net/html5/HelloWebWorkersJSON_EN.htm

Compatibilidad de los navegadores

Los Web Workers acaban de aparecer en la versión preliminar de IE10 (IE10 Platform Preview). También están soportados en Firefox (desde la versión 3.6), Safari (desde la 4.0), Chrome y Opera 11. No obstante, las versiones de móviles de estos navegadores no los soportan. Si quieres consultar una tabla de compatibilidad más detallada, la puedes ver: http://caniuse.com/#search=worker

Para saber sobre la marcha si esta funcionalidad está soportada en el navegador desde tu propio código, puedes utilizar el mecanismo de detección de funcionalidad (¡no te recomiendo que utilices sistemas de detección del agente de usuario!).

Tienes dos posibles soluciones que te ayudarán. La primera es una simple comprobación de la funcionalidad por ti mismo, con este sencillo código:

/* Comprueba si el navegador soporta Web Workers */
if (window.Worker) {
    // Aquí puedes añadir código para utilizar Web Workers
}

La segunda es utilizar la famosa librería Modernizr (que ahora viene como componente nativo con las plantillas de proyecto MVC3 de ASP.NET). Después puedes utilizar un código sencillo, como este:

<script type="text/javascript">
    var divWebWorker = document.getElementById("webWorkers");
    if (Modernizr.webworkers) {
       divWebWorker.innerHTML = "Web Workers SI están soportados";
    }
    else {
       divWebWorker.innerHTML = "Web Workers NO están soportados";
    }
</script>

Este mecanismo te permitirá exponer dos versiones de tu aplicación. Si no están soportados los Web Workers, simplemente ejecutas Javascript como siempre. Si están soportados, puedes pasar parte del código Javascript a los workers para mejorar el rendimiento de la aplicación en los navegadores más recientes. No vas a tener que romper nada ni desarrollar versiones específicas para los navegadores de última generación. El mismo código puede funcionar en todos, pero con diferencias de rendimiento en cada caso.

Elementos inaccesibles desde un worker

En vez de mirar a aquello a lo que no podemos acceder desde los Workers, mejor examinamos a qué cosas sí tenemos acceso:


Nota: Esta tabla la he obtenido de nuestra documentación de MSDN: HTML5 Web Worker

En resumen no tenemos acceso al DOM. Este diagrama nos lo resume bastante bien:

Por ejemplo, puesto que no tenemos acceso al objeto window desde un worker, no podemos acceder a Local Storage (que no parece tampoco ser compatible con el modelo de acceso concurrente thread-safe). Estas limitaciones pueden parecer demasiado restrictivas para los programadores acostumbrados a operar con múltiples threads en otros entornos, pero su gran ventaja es que no van a caer en los mismos problemas que solían antes: bloqueos, condiciones de carrera, etc. Con los Web Workers no tenemos que preocuparnos de nada de esto, y gracias a ello, la tecnología de Web Workers es sumamente accesible, y nos permite además mejorar notablemente el rendimiento de las aplicaciones en ciertos casos.

Depuración y manejo de errores

El manejo de errores de los Web Workers es muy sencillo, basta con suscribirse al evento OnError igual que hemos hecho antes con el evento OnMessage:

myWorker.addEventListener("error", function (event) {
    _output.textContent = event.data;
}, false);

Si esto es lo mejor que nos pueden ofrecer los Web Workers por diseño para depurar el código… es muy limitado ¿no?

La barra de desarrollo F12 para una depuración más avanzada

Para quien necesita algo más, IE10 nos permite depurar directamente el código de los Web Workers dentro del depurador de script igual que hace con todos los demás scripts.
Para ello tenemos que arrancar la barra de herramientas de desarrollo (pulsando la tecla F12) y pulsar en la pestaña “Script”. Aún no podremos ver el archive JS asociado con el worker, pero después de pulsar en “Start debugging” aparecerá como por arte de magia:

El paso siguiente es depurar el worker igual que hacemos con el código Javascript de toda la vida.

IE10 es a día de hoy el único navegador que nos permite hacer esto. Si quieres saber más sobre esta funcionalidad, te recomiendo el artículo Debugging Web Workers in IE10

Una solución interesante para imitar al método console.log()

En definitiva, tienes que saber que el objeto console no está disponible dentro del worker. Por tanto, si necesitas trazar lo que sucede dentro de uno de ellos mediante el método .log(), no va a funcionar, ya que el objeto console no está definido. Por suerte, he encontrado un ejemplo interesante que imita la funcionalidad de console.log() utilizando el MessageChannel: console.log() for Web Workers. Funciona bastante bien con IE10, Chrome y Opera pero no con Firefox, ya que este navegador aún no soporta MessageChannel.

Nota: para que el ejemplo de ese enlace funcione en IE10, hay que cambiar esta línea del código:

console.log.apply(console, args); // Pasa los argumentos al log real

por esta otra:

console.log(args); // Pasa los argumentos al log real

Después podrás obtener resultados como estos:

DEMO: si quieres ver en acción una simulación de console.log() visita esta página: http://david.blob.core.windows.net/html5/HelloWebWorkersJSONdebug.htm

Escenarios de uso y detección de candidatos potenciales

¿Cuándo conviene utilizar Web Workers?
Cuando buscamos en la Web ejemplos de uso de los Web Workers, siempre encontramos el mismo tipo de demos: computación intensiva de tipo matemático o científico. Veremos algunos de simulación de reflejos de la luz, fractales, números primos y cosas así en Javascript. Hermosas demos que sirven para saber cómo funcionan los workers, pero que nos dan una panorámica más bien escasa sobre las posibilidades de su uso en aplicaciones del “mundo real”.
Es cierto que las limitaciones que hemos visto antes sobre el acceso a recursos desde dentro de los Web Workers reducen sensiblemente el número de escenarios potenciales. Aun así, si dedicamos un poco de tiempo a pensar en ello, en seguida aparecen nuevos casos de uso, algunos ciertamente interesantes:
  • Procesamiento de imágenes mediante el uso de datos extraídos de elementos <canvas> o <video>. Podemos dividir la imagen en varias zonas y pasárselas a diferentes workers que trabajarán en paralelo. Podemos aprovechar toda la potencia de la nueva generación de CPUs multicore. Cuantos más núcleos, más rápido irá el proceso.
  • Manipulación de grandes cantidades de datos obtenidos de distintos sitios y que necesitamos analizar después de una llamada a XMLHTTPRequest. Si el tiempo necesario para procesar los datos es importante, mejor lo hacemos en segundo plano, con un Web Worker para evitar que se cuelgue el thread de la UI..
  • Análisis de textos en segundo plano: ya que en teoría tenemos más tiempo de CPU disponible al utilizar los Web Workers, podemos imaginar nuevos escenarios de uso con Javascript. Por ejemplo, la revisión en tiempo real de lo que escribe el usuario, sin afectar a la experiencia de la UI. Aplicaciones como Word (o por ejemplo todas las que componen la suite Office Web Apps) pueden mejorarse ejecutando en segundo plano tareas como la corrección ortográfica mientras escribe, búsqueda de sinónimos, traducción simultánea de párrafos, etc.
  • Accesos simultáneos a una base de datos. IndexDB nos permitirá hacer aquello que Local Storage no nos permite: un entorno de almacenamiento de datos desde los Web Workers en modo concurrente (thread-safe).
Pero voy más lejos: si nos adentramos en el mundo de los videojuegos, podemos imaginarnos las posibilidades de uso de motores de inteligencia artificial y física desde los Web Workers. Por ejemplo, he encontrado un modelo experimental aquí: On Web Workers, GWT, and a New Physics Demo que utiliza el motor de física Box2D physic engine con Workers. Si quieres utilizar un motor de Inteligencia Artificial, con los Workers podrás procesar en el mismo marco de tiempo una cantidad de datos mayor (por ejemplo anticipando una cantidad mayor de movimientos en el juego de ajedrez).

¡Algunos compañeros míos pueden afirmar ya que el único límite es nuestra imaginación!
Pero de forma general, siempre que no necesitemos el DOM, cualquier código Javascript de procesamiento lento que pueda afectar a la experiencia del usuario es buen candidato a Web Worker. Sin embargo tenemos que tener presentes tres puntos importantes:

  1. El tiempo de inicialización y el que se necesita para comunicarse con el worker no puede ser superior al tiempo del procesamiento en sí.
  2. La cantidad de memoria que necesitaremos para utilizar simultáneamente varios workers
  3. La dependencia mutua de los bloques de código en el caso de que se necesite algún tipo de sincronización. ¡El paralelismo no es nada fácil, querido amigo!
Por mi parte, hace poco he publicado una demo que se llama Web Workers Fountains:

En este efecto se muestran efectos de algunas partículas (las fuentes) y utiliza un web worker por cada fuente para calcular las partículas de la forma más rápida posible. El resultado de cada worker se consolida con los demás para visualizarlo en un elemento <canvas>. Los Web Workers también se pueden intercambiar mensajes entre ellos utilizando los Canales de Mensaje. En esta demo se utilizan para preguntar a cada uno de los workers si es el momento de cambiar el color de las Fuentes. Después el programa avanza en un bucle con esta secuencia de colores: rojo, naranja, amarillo, verde, azul, violeta y rosa, utilizando los Canales de Mensaje. Si te interesa conocerlo en detalle, ve a la función LightManager() del archivo Demo3.js.

Puedes reproducir esta demo en Internet Explorer 10, ¡es muy interesante como ejemplo para jugar con ella!

Cómo identificar los puntos calientes en tu código

Para controlar posibles cuellos de botella e identificar las partes del código que podríamos independizar como Web Workers, puedes utilizar el perfilador de scripting que se incluye con la barra F12 del IE9 e IE10. Te ayudará a identificar tus “puntos calientes”. De todas formas, la identificación de estas secciones de código no quiere decir que sean buenos candidatos a migrar a un worker. Podrás entenderlo mejor con estos dos ejemplos:

Caso 1: animación dentro de un <canvas> con la demo de Lectura Rápida

Esta demo la he sacado de IE Test Drive y se puede ver directamente aquí: Speed Reading. Intenta mostrar, a la mayor velocidad posible, algunos caracteres utilizando el elemento <canvas>. El objetivo consiste en poner a prueba la calidad de la implementación de la capa de aceleración por hardware de un navegador. Pero si vamos un paso más allá, ¿podríamos mejorar su rendimiento distribuyendo algunas operaciones en threads independientes? Necesitamos antes analizar el problema un poco.

Si ejecutas esta demo en IE9/10, puedes arrancar también el perfilador en un par de segundos. EL resultado que puedes obtener sería más o menos este:

Si clasificamos las funciones en orden de mayor consume de tiempo a menor, se ve claramente que las funciones que más tiempo consumen son DrawLoop(), Draw() y drawImage(). Si pulsamos con el ratón en la línea Draw, nos conduce al código de este método. Ahí podemos ver algunas llamadas de este tipo:

surface.drawImage(imgTile, 0, 0, 70, 100, this.left, this.top, this.width, this.height);

Donde el objeto surface está referenciando el elemento %lt;canvas>.

Una conclusión rápida de este breve análisis es que esta demo dedica la mayor parte del tiempo a dibujar dentro del lienzo utilizando el método drawImage() . Puesto que el elemento <canvas> no es directamente accesible desde un Web Worker, no vamos a poder descargar esta tarea tan intensiva distribuyéndola entre distintos threads (podríamos haber ideado alguna forma alternativa de gestionar el elemento <canvas> de forma concurrente, por ejemplo). Esta demo, por tanto, no es buena candidata para aprovechar las posibilidades de paralelismo que nos permiten los Web Workers.

Pero ilustra muy bien el proceso que tenemos que seguir. Si, después de perfilar nuestro código, descubrimos que la parte principal de los scripts que se llevan casi todo el tiempo de ejecución está ligada estrechamente con objetos del DOM, los web workers no nos van a ayudar a mejorar el rendimiento de la aplicación.

Caso 2: rayos de luz en el <canvas>

Vamos a ver este otro sencillo ejemplo. Se trata de un modelo lumínico que podemos ver en Flog.RayTracer Canvas Demo. Un simulador como este utiliza cálculos matemáticos que consumen una gran cantidad de CPU para reproducir las trayectorias de la luz. La idea consiste en simular efectos tales como reflejos, refracciones, brillos, distorsiones de imagen, etc.

Vamos a mostrar la escena con el perfilador en marcha. Lo que obtenemos es algo así:

De nuevo, si clasificamos las funciones en orden decreciente por el tiempo consumido, vemos que hay dos funciones que claramente ocupan casi todo el tiempo: renderScene() y getPixelColor().

El método getPixelColor() se utiliza para calcular el pixel actual. El caso es que el simulador está representando la escena pixel a pixel. Este método getPixelColor() llama después al método rayTrace() que se encarga de representar las sombras, luz ambiente y demás efectos. Este es el núcleo de nuestra aplicación, y si revisamos el código de la función rayTrace() podemos ver que se trata de 100% materia Javascript. Este código no tiene dependencias del DOM. Muy bien, creo que ya lo tenemos: este ejemplo sí será un buen candidato al paralelismo. Y aún más: podemos dividir fácilmente la restitución de la imagen en diversos threads (e incluso en varias CPUs en teoría) puesto que no se necesita sincronización entre las operaciones de computación de un pixel y las de los demás. Cada pixel se calcula de forma independiente de sus vecinos y no se utiliza ningún tipo de técnica anti-aliasing en esta demo.

Así que no es sorprendente que podamos encontrar algunos ejemplos de simuladores de efectos lumínicos utilizando varios Web Workers como en este caso: http://nerget.com/rayjs-mt/rayjs.html

Después de perfilar este simulador en IE10, podemos ver diferencias muy sustanciales entre el modelo sin workers y otro que arranca hasta 4 workers a la vez:

En la primera pantalla, el método processRenderCommand() está consumiendo prácticamente toda la CPU disponible y la escena se restituye en 2,854 segundos.

Con 4 Web Workers, el método processRenderCommand() se ejecuta en paralelo en 4 threads distintos. Podemos incluso ver su Worker Id en la columna de la derecha. La escena se restituye en 1,473 segundos. La ventaja es notoria: aumenta la velocidad al doble.

Conclusión

No hay ningún concepto nuevo ni magia con respecto a los Web Workers en cuanto a la forma en que deberíamos revisar/rediseñar nuestro código Javascript para hacerlo apto para la ejecución en paralelo. Necesitaremos aislar la parte del código que consume más CPU y esta parte debe ser relativamente independiente del resto de la lógica de la página para evitar tener que esperar a la sincronización de tareas. Y lo más importante: el código debe ser independiente del DOM. Si ambas condiciones se cumplen, es recomendable utilizar Web Workers, ya que con ellos vamos con toda seguridad a mejorar el rendimiento global de la aplicación.

Recursos adicionales

Estos son algunos recursos interesantes que te recomiendo: