Uno de mis últimos encargos consiste en una aplicación financiera para el navegador. En resumen, se trata de “traducir” una serie de hojas en Excel a javascript. El cómo lo he implementado da para otro artículo.

Básicamente el funcionamiento es el siguiente:

  • Creamos un campo de texto por cada celda del fichero Excel.
  • Muchas celdas tienen valores dinámicos, es decir, se generan a partir del valor de otras.
  • Definimos la fórmula que genera el valor de cada celda dinámica, y esa fórmula la añadimos a la pila de callbacks del evento ‘change’ de todas las celdas de las que depende.

Problema: Hay miles de celdas, prácticamente todas dinámicas. Esto implica que a menudo, al cambiar un sólo valor, se generan cientos o miles de cálculos. Javascript es single threaded, es decir, sólo se ejecuta una única hebra (para realizar cálculos, modificar el árbol DOM, gestionar eventos, o redibujar la pantalla). Si una operación muy costa en cálculos conlleva 8 segundos, la interfaz de usuario se quedará bloqueada hasta que finalice. Mal asunto.

El experto en usabilidad Jakob Nielsen dijo:

0,1 segundos es el límite para que el usuario perciba que el sistema responde inmediatamente.

¿Son los Web Workers la solución?

Quizás te preguntes qué son los Web Workers. Se trata de una especificación de WHATWG, el equipo que ha dado forma a lo que hoy conocemos como HTML5, para permitir programar aplicaciones multihebra en javascript, es decir, correr procesos en segundo plano.

Si bien es una funcionalidad que han empezando a incorporar recientemente los navegadores (a excepción de IE9), no es nada nuevo; el paquete Google Gears ya ofrecía estos mecanismos. De hecho, los web workers están basados en la API WorkerPool de Google Gears.

Por tanto, ¿son la solución al problema? Sí, pero…

A pesar de que una hoja de cálculo puede ser un ejemplo de libro para el uso de Web Workers, finalmente no los he utilizado. Existen algunas limitaciones, por cuestiones de seguridad, a la hora de usar Web Workers. A destacar, que no tiene acceso a:

  1. DOM.
  2. objeto window.
  3. objeto document.
  4. objeto parent.

Toda la comunicación con la hebra principal se realiza mediante mensajes. Esto interfiere con el código que ya tenía, de modo que implicaría reescribirlo en gran parte. No es una opción por desgracia.

Luego está el motivo de que no funcionan ni funcionarán en Internet Explorer.

Timers al rescate

Repasemos conceptos de sistemas operativos. En un sistema multitarea, en un momento dado no se pueden ejecutar más tareas que procesadores tiene el equipo. Por eso existe el planificador de procesos, que es la parte que se encarga de repartir el tiempo disponible entre los distintos procesos en ejecución. Si se asignan tiempos bajos de ejecución, el efecto será que están funcionando varios procesos concurrentemente. Sintetizando muchísimo, podríamos decir que el planificador de procesos se encarga de asignar bloques de tiempo a cada proceso.

Ahora repasemos los timers. ¿Qué hacen exactamente? Posponen la ejecución de un comando. Esto significa que, mientras se espera ese tiempo, el navegador queda libre para hacer otras cosas.

¿Y si añadimos pausas voluntarias en la propagación de eventos ‘change’ de las celdas? De esta forma, habrá pausas que permitirán “descongelar” la interfaz de usuario, emulando que los cálculos se están ejecutando en un segundo plano, haciendo que la interfaz de usuario sea fluida. Miremos el siguiente gráfico:

Gráfico de relación de tiempos con y sin timers

El color azul representa los cálculos. El verde, es cuando no realizamos cálculos y la interfaz responde a eventos (el equivalente a este proceso en sistemas operativos se llama idle).

En el primer caso no utilizamos timers. Lo que ocurre es que el navegador se queda congelado durante más de tres segundos.

Sin embargo, en el segundo caso añadimos pausas voluntarias, con lo que percibimos que en todo momento el navegador reacciona inmediatamente a nuestras acciones.

Demo

He hecho una sencilla demo. El script se encarga de calcular los números primos menores de 300.000. En un caso con timers y en otro sin él:

  1. Sin timers. La página se quedará congelada hasta que finalice el cálculo.
  2. Con timers. Podrás interactuar con la página mientras hace los cálculos.

Si miramos el código, veremos que hay algunas diferencias. En el primer caso, esto es lo que ocurre:

jQuery("#button").bind('click', function() {
  for (var i=1; i<=300000; i++) {
    if(isPrime(i)) {
      jQuery('#prime').append(" "+ i);
    }
  }

Comprobamos uno a uno cada número desde un bucle, y si es primo, lo mostramos en pantalla. Seguramente haga saltar un aviso en el navegador de que el script está llevando demasiado tiempo.

El segundo ejemplo es un poco más elaborado:

function comprobarPrimo(numero, fin,total) {
  var delay = 10;
  var heap = 500;
  if (isPrime(numero)) {
    jQuery('#prime').append(" "+ numero);
  }
  if(++numero < fin) {
    if (total%heap === 0) {
      setTimeout("comprobarPrimo("+numero+","+fin+","+(total+1)+")", delay);
    } else {
      comprobarPrimo(numero, fin, (total+1));
    }
  }
}

jQuery("#button").bind('click', function() {
  comprobarPrimo(1,300000,0);
});

En lugar de usar un bucle, usamos una función recursiva. Esto lo hacemos porque, cada vez que llamamos a setTimeout, se ejecuta el código que le sigue sin esperar el delay. Es decir, que al añadir un timer no pararíamos realmente toda la ejecución. Con una función recursiva corregimos esto.

Y definimos dos variables: delay, que es el retardo en milisegundos que se aplicará, y heap, que define cada cuántas llamadas se aplica delay. En este ejemplo se establece un delay de 20 milisegundos que se aplica cada 500 números primos. Los otros 499 se ejecutan sin retardo.

El resultado es completamente distinto al del primer ejemplo. Y únicamente añadimos pausas de 20 milisegundos cada 500 números primos. Por supuesto el tiempo de ejecución se ve incrementado.