HTML5 Background tasks using Web Workers

imageOne of the problems with running JavaScript in the browser is that everything usually executes on the same thread as the UI. With most scripts this is fine because they are short executing however if you start doing more complex calculations you might run into the vase where the UI becomes non responsive because of the JavaScript executing. And when that happens you will see one of these dialogs popup and you are at the mercy of the end user, not the best of places to be.

 

Update: Check out these live Simple Web Worker and Chunked Web Worker demos.

 

Fortunately there is a fix for this in the form of the new HTML5 Web Workers specification

Web workers are relatively well supported, IE9 being the big exception, as you can see from caniuse.com. Getting started with them is quite straightforward, basically you create a Worker object and pass it the URL of the script you want to execute. This has to be a URL, you can’t just pass a function, something that would have been handy.

The following HTML page contains all the code required.

   1: <!DOCTYPE html>


   2: <html>


   3: <head>


   4:     <title></title>


   5: </head>


   6: <body>


   7:     <button id='btnStart'>


   8:         Start</button>


   9:     <button id='btnStop'>


  10:         Stop</button>


  11:     <button id='btnPause'>


  12:         Pause</button>


  13:     <button id='btnResume'>


  14:         Resume</button>


  15:     <ul id='primes'>


  16:     </ul>


  17:     <script src="Scripts/jquery-1.7.2.js" type="text/javascript"></script>
   1:  
   2:     <script>
   3:         $(function () {
   4:             var worker = null;
   5:             $('#btnStart').click(function () {
   6:                 if (!worker) {
   7:                     $('#primes').empty();
   8:                     if (!!window.Worker) {
   9:                         worker = new Worker('Scripts/Primes.js');
  10:                         worker.onmessage = function (e) {
  11:                             $('<li>').text(e.data).prependTo('#primes');
  12:                         };
  13:                     } else {
  14:                         $('<li>').text('Web Worker is not supported :-(').prependTo('#primes');
  15:                     }
  16:                 }
  17:             });
  18:  
  19:             $('#btnStop').click(function () {
  20:                 if (worker) {
  21:                     worker.terminate();
  22:                     worker = null;
  23:                     $('<li>').text('Terminated').prependTo('#primes');
  24:                 }
  25:             });
  26:  
  27:             $('#btnPause').click(function () {
  28:                 if (worker) {
  29:                     $('<li>').text('Pause').prependTo('#primes');
  30:                     worker.postMessage('pause');
  31:                 }
  32:             });
  33:  
  34:             $('#btnResume').click(function () {
  35:                 if (worker) {
  36:                     $('<li>').text('Resume').prependTo('#primes');
  37:                     worker.postMessage('resume');
  38:                 }
  39:             });
  40:         });
  41:  
  42:     
</script>


  18: </body>


  19: </html>

 

The important part is the start button:

   1: worker = new Worker('Scripts/Primes.js');


   2: worker.onmessage = function (e) {


   3:     $('<li>').text(e.data).prependTo('#primes');


   4: };

This code creates a Worker object and points it to the JavaScript file to start executing. In this case we are going to calculate prime numbers.

As soon as the Worker has loaded the JavaScript file it will start executing, no need to do anything special.

 

Communicating with Web Workers

The code executing as part of the Worker is completely isolated from the UI document. You can send messages between the two using the postMessage() function but all data will be cloned, no shared references exist. When a message has been posted the other party can specify the onmessage callback and react to the message. The worker has restrictions to the data being passed, the most important one is that nothing related to the DOM can be passed to the worker object and everything that can be is cloned first.

image

A simple implementation of the worker to calculate prime numbers looks something like this. Note the postMessage being used to pass the computed prime numbers back to the user interface,

   1: for (var i = 2; i < 1000000; i++) {


   2:     var isPrime = true;


   3:     for (var d = 2; d < i; d++) {


   4:         if (i % d === 0) {


   5:             isPrime = false;


   6:             break;


   7:         }


   8:     }


   9:  


  10:     if (isPrime) {


  11:         postMessage(i);


  12:     }


  13: }

 

 

Controlling the workers execution

Suppose we want to stop the workers execution there is a terminate() function on the worker which is used in the Stop button.

   1: $('#btnStop').click(function () {


   2:     if (worker) {


   3:         worker.terminate();


   4:         worker = null;


   5:         $('<li>').text('Terminated').prependTo('#primes');


   6:     }


   7: });


This will kill the worker process and that is the end of it, there is no way to restart it. Nice, simple and effective however sometimes that is not quite what we want and we want to restart the worker.

 

Suspending and resuming a worker

Given the fact that we can send messages between the UI and the worker the code below might look like a valid approach. Unfortunately this doesn’t quite work Sad smile

   1: var isPaused = false;


   2:  


   3: onmessage = function (e) {


   4:     if (e.data == 'pause') {


   5:         isPaused = true;


   6:     }


   7: };


   8:  


   9: for (var i = 2; i < 1000000; i++) {


  10:     var isPrime = true;


  11:     for (var d = 2; d < i; d++) {


  12:         if (i % d === 0) {


  13:             isPrime = false;


  14:             break;


  15:         }


  16:     }


  17:  


  18:     if (isPrime) {


  19:         postMessage(i);


  20:     }


  21:  


  22:     if (isPaused) {


  23:         break;


  24:     }


  25: }

 

The problem here is that the postMessage()/onmessage pair works using the event loop and as long as the Web Worker is busy calculating it will never receive the message, instead it will be queued and remain queued until the worker is idle, something that doesn’t happen until all primes have been calculated.

 

Using  a chunking algorithm

The solution is to use a chunking algorithm. This algorithm brakes the calculation into different groups and use the setTimeout() API to execute the next chunk after a small delay. The result of using setTimeout() is that the message posted can be read and we can actually pause and resume the worker execution. Using a chunking algorithm out background worker JavaScript looks like this:

   1: var isPaused = false;


   2:  


   3: onmessage = function (e) {


   4:     if (e.data == 'pause') {


   5:         isPaused = true;


   6:     } else if (e.data == 'resume') {


   7:         isPaused = false;


   8:         testPrime();


   9:     }


  10: };


  11:  


  12: var i = 2;


  13: testPrime();


  14:  


  15: function testPrime() {


  16:     var isPrime = true;


  17:     for (var d = 2; d < i; d++) {


  18:         if (i % d === 0) {


  19:             isPrime = false;


  20:             break;


  21:         }


  22:     }


  23:  


  24:     if (isPrime) {


  25:         postMessage(i);


  26:     }


  27:  


  28:     if (!isPaused) {


  29:         if (i < 1000000) {


  30:             i++;


  31:             setTimeout(testPrime, 0);


  32:         }


  33:     }


  34: }


With this in place we can pause and resume our prime number calculation Smile

 

image

The only problem with this approach is that, in some browsers, it slows down calculations a lot. So instead of doing a setTimeout() after every prime you might want to do so once after a series of numbers has been checked. For example with the code below:

   1: var i = 2;


   2: testPrime();


   3:  


   4: function testPrime() {


   5:     var isPrime = true;


   6:     for (var d = 2; d < i; d++) {


   7:         if (i % d === 0) {


   8:             isPrime = false;


   9:             break;


  10:         }


  11:     }


  12:  


  13:     if (isPrime) {


  14:         postMessage(i);


  15:     }


  16:  


  17:     if (!isPaused) {


  18:         if (i < 1000000) {


  19:             i++;


  20:             if (i % 100 === 0) {


  21:                 setTimeout(testPrime, 0);


  22:             } else {


  23:                 testPrime();


  24:             }


  25:         }


  26:     }


  27: }



 



How about browsers that don’t support Web Workers?



It turns out that using this chunking approach in the UI is a very decent alternative to using a Web Worker as it prevents the user interface from completely blocking. It will still be somewhat slower to respond but is the best you can do in the circumstances.



 



Update: Check out these live Simple Web Worker and Chunked Web Worker demos.



 



Enjoy!

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>