Passion & Opportunity ? continue : break

How to use WebWorker in HTML5?
Written: 2019-03-27 11:46:42 Last update: 2019-06-15 23:05:54

Web Worker is one of the new HTML5 Web APIs, it is a way to run a process (Javascript function) in different thread (not UI main thread), using different thread means that long process will not block the UI (freeze).

To create a Web Worker we need to create a different JS file, such as 'mywebworker1.js', below is a demonstration for sending text and updating progress bar from Worker:

self.addEventListener('message', function(e) {

  // check what is the data type
  let dataType = typeof(e.data);
  if(dataType == 'string') {
    console.log('worker received STRING: ' + e.data);
    
    // .. do something 

    // send received confirmation 
    self.postMessage('worker processed received text already');
  } else if(dataType == 'object') {
    console.log('worker received an OBJECT: ' + e.data);

    // process it
    if(e.data.action == 'count') {
      let from = e.data.from;
      let to = e.data.to;

      console.log('going to start counting from ' + from + ' to ' + to);

      // current progress
      let now = from;

      // set interval to send progress
      let myinterval = setInterval(function() {
        // send event
        self.postMessage({'from': from, 'to': to, 'now': now});

        if(now >= to) {
          clearInterval(myinterval);
          console.log('counting is finished at ' + now);
        } else {
          console.log('counting: ' + now);
        }

        // increase counter
        now++;
      }, 1000); // every 1 second
    }
  }
}, false);

Next step is to create a Web Worker instance (load the JS file), such as:

var mywebworker1 = new Worker('mywebworker1.js');
Register the message listener to receive data from Worker (this code has a little logic to update Progress Bar for sample)
// register event listener to receive data from worker
mywebworker1.addEventListener('message', function(e) {
  // at this moment, this is in main UI (not in thread)
  let dataType = typeof(e.data);
  if(dataType == 'string') {
    console.log('Received text from worker: ' + e.data);

  } else if(dataType == 'object') {

    // update Progress Bar
    let progress = (100 * ((e.data.now - e.data.from) + 1)) / ((e.data.to - e.data.from) + 1);
    var elePB1 = document.getElementById('my-inner-progress-bar-1');
    elePB1.style.width = progress + '%';
    elePB1.innerText = e.data.from + ' > ' + e.data.now + ' > ' + e.data.to;
  }
});
After the worker is created then we can ask it to do whatever we want using 'postMessage' method, such as:
// send a simple text
mywebworker1.postMessage('Hello there !');

For simple sending text, please type a sentence in input box below then click send to Worker, see the result in browser's console.

Another example for demonstration is to do long progress in Worker, the Worker will send progress update to main UI thread to update progress bar, such as:

//send custom data
mywebworker1.postMessage({'action': 'count', 'from' : 1, 'to': 10});

For simple Web Worker, we use a simple 'hack' to create an embedded WebWorker using JS code inside the same file, such as:

function createEmbeddedWebWorker(fullFunctionCode) {
  if(window.Worker) {
    // browser support Web Worker, so create it

    // create a Blob of a function name to be a 'file' (fake file)
    let b = new Blob(['onmessage=' + fullFunctionCode], { type: 'text/javascript'});

    // create WebWorker from blob file
    let worker = new Worker(URL.createObjectURL(b));
    return worker;

  } else {
    alert('Sorry, your browser does NOT support Web Worker');
    return null;
  }
}

function embeddedWebWorkerFunction(e) {
  // check what is the data type
  let dataType = typeof(e.data);
  if(dataType == 'string') {
    console.log('worker received STRING: ' + e.data);
    
    // .. do something 

    // send received confirmation 
    self.postMessage('worker processed received text already');
  } else if(dataType == 'object') {
    console.log('worker received an OBJECT: ' + e.data);

    // process it
    if(e.data.action == 'count') {
      let from = e.data.from;
      let to = e.data.to;

      console.log('going to start counting from ' + from + ' to ' + to);

      // current progress
      let now = from;

      // set interval to send progress
      let myinterval = setInterval(function() {
        // send event
        self.postMessage({'from': from, 'to': to, 'now': now});

        if(now >= to) {
          clearInterval(myinterval);
          console.log('counting is finished at ' + now);
        } else {
          console.log('counting: ' + now);
        }

        // increase counter
        now++;
      }, 1000); // every 1 second
    }
  }
}

// create variable for embedded worker as global variable, 
// so we can delete it later
var mWebWorker2;
function startProgressBarUsingEmbeddedWebWorker() {
  
  if(! (mWebWorker2)) {
    console.log('Worker instance is not exist, so create it');
    mWebWorker2 = createEmbeddedWebWorker(embeddedWebWorkerFunction.toString());
  } else {
    if(mWebWorker2.isBusy) {
      console.log('startProgressBarUsingEmbeddedWebWorker(), web worker is busy, please call this function again later.');
      return;
    }
    console.log('startProgressBarUsingEmbeddedWebWorker(), Worker instance is already existed, going to send data.');
  }

  // define callback
  mWebWorker2.onmessage = function(e) {
    // at this moment, this is in main UI (not in thread)
    let dataType = typeof(e.data);
    if(dataType == 'string') {
      console.log('Received text from worker: ' + e.data);

    } else if(dataType == 'object') {

      let progress = (100 * ((e.data.now - e.data.from) + 1)) / ((e.data.to - e.data.from) + 1);
      var elePB2 = document.getElementById('my-inner-progress-bar-2');
      elePB2.style.width = progress + '%';
      elePB2.innerText = e.data.from + ' > ' + e.data.now + ' > ' + e.data.to;

      if(e.data.now == e.data.to) {
        // already finished, so reset busy flag
        mWebWorker2.isBusy = false;
      }
    }
  }

  // send command to start counting
  mWebWorker2.postMessage({'action': 'count', 'from' : 7, 'to': 22});

  // add new (if not created yet) property and init value
  mWebWorker2.isBusy = true; 
}

function deleteWebWorker2() {
  console.log('deleteWebWorker2()');
  if(mWebWorker2) {
    mWebWorker2.terminate(); // delete instance
    mWebWorker2 = undefined; // remove variable from memory
  }
}
NOTE:
  • We call 'startProgressBarUsingEmbeddedWebWorker()' in the 'Start Progress Bar with Embedded WebWorker' button
  • The problem with embedded Worker is we can NOT set breakpoint for debugging, so during development it is better to use external JS file then after finished we can embedded it.
  • This page only showing simple example with logic to communicate between caller and Worker like in updating progress bar.
  • Be careful before create new Worker ('= new Worker(...)'), need to check whether there is existing Worker or not, a new instance will be created every time and will consume memory but maybe never used, to purposely share a Worker then we need to use SharedWorker.
  • Worker is a single thread, so when it is busy doing long task then it will not be able to receive and process message queue.
  • Remember to 'terminate' (delete) any Worker if its job already finished.
  • After a Worker is 'terminated' then it will not be able to process any incoming postMessage(), there is no warning nor error, so please check the code properly, it must be re-created to be able to handle postMessage() again.
  • 'terminate' will force the Worker to end immediately and will not wait to finish any loop or any pending process inside Worker, so please use with careful to avoid incomplete writing to I/O (file or network).


Keep calm and hack the planet