HTML5 Webworker best practices

Posted: Saturday, August 18, 2012 by Venkat Pola in Labels:
0


Recently i explored HTML5 web workers and this caught my interest. Am very much excited to use it in performing heavy computation tasks in Javascript. Webworkers really smashes the single thread bottle neck problem in Javascript to some extent. Would like to share few thoughts on how i organized my code base with respect to my requirements.

Requirement 1:-

I had a hard requirement to have frequent communication between the page where the worker is spawned and the worker where all of my business logic resides. Due to which i had to call different functions multiple times to and fro.

So i don't want to bloat up the "worker.onmessage & self.onmessage" callbacks in both parent page and worker with lot of if..else with some conditions.

Here is what i come up with some smart approach where i defined the data structure something like below

{"cmd" : "func_call" , "scope" : ["add", "Controller","task1"],"params" : [1,2]}

If the command is func_call then i will fetch function object specified in the scope array right staring from the "self" as base scope object and is called with the respective params given in the array. In the below code you can find function "callFunction" which takes scope and params arrays and calls respective function with necessary params. Please refer to "worker.onmessage & self.onmessage" callbacks in the below sample code for the usage.

Requirement 2:-

I don't want to mess up my business logic JS files with the web worker specific code because these might be used by non worker application.

"importScirpts" is very helpful to accomplish it.

importScripts.apply(self,["task1.js","task2.js"]); // Assuming task1.js , task2.js are business logic files

Requirement 3:-

I too had to send too much JSON data with deep hierarchy in it between base page & worker. Even though "postMessage" inbuilt supports sending JSON objects in it. I found that sending big JSON's with too much hierarchy in it is causing some problems in webkit and in the receiver side it is coming as empty string.

I suggest to send the JSON data by manually stringifying it.
worker.postMessage(JSON.stringify({"a":{"b":"c"}}));

Requirement 4:-

If we try to load the worker js files using file:// protocol for testing purpose then it will end up with "Uncaught Error: SECURITY_ERR: DOM Exception". You can avoid it by opening chrome.exe with command line param --allow-file-access-from-files

"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --allow-file-access-from-files

Below is the sample example alternatively you can download the code from my github account here Worker-Best-Practices
 
index.html - Base page where the web-worker is spawned
myTaskWorker.js - Worker file
task1.js,task2.js - Contains business logic.


index.html

<html>
<head>
<script>

var loggerDiv;
var postToWorker = function(worker , data){
worker.postMessage(JSON.stringify(data));
};

var callFunction = function(funcScope , params){
for(var i =funcScope.length - 1  , funcObj = self; i>=0 ; i-- ){
if(funcObj) funcObj = funcObj[funcScope[i]];
}
if(typeof funcObj === "function") return funcObj.apply(this , params);
};

var logMessage = function(msg){
loggerDiv.innerHTML+=msg+" < br/>";
};

var worker = new Worker("./js/myTaskWorker.js"),
jsFiles = ["./task1.js","./task2.js"];

worker.onmessage = function(event) {
var instruction = event.data;
instruction = JSON.parse(instruction);

if(instruction["cmd"]==="func_call"){ // Worker trying to talk to function in base page
callFunction(instruction["scope"] , instruction["params"]);
}
 
};

worker.onerror = function(e){
console.log("Exception in Worker : "+e.message);
};


var onPageLoad = function(e){
loggerDiv = document.getElementById("log");
postToWorker(worker,{"cmd" : "start" , "data" : jsFiles}); // Start the worker.

postToWorker(worker,{"cmd" : "func_call" , "scope" : ["add", "Controller","task1"],"params" : [1,2]}); // Calls function directly in worker task1.Controller.getInfo()
postToWorker(worker,{"cmd" : "func_call" , "scope" : ["multiply", "Controller","task2"],"params" : [3,4]}); // Calls function directly in worker task1.Controller.getInfo()

}
</script>
</head>
<body onLoad="onPageLoad();">
<div id="log"> </div>
</body>
</html>

myTaskWorker.js

var postToParent = function(data){
self.postMessage(JSON.stringify(data));
};


var callFunction = function(funcScope , params){
for(var i =funcScope.length - 1  , funcObj = self; i>=0 ; i-- ){
if(funcObj) funcObj = funcObj[funcScope[i]];  
}
if(typeof funcObj === "function") return funcObj.apply(this , params);
};
    
self.onmessage = function(event) {
var instruction = event.data;
instruction = JSON.parse(instruction);
if(instruction["cmd"]==="start"){
importScripts.apply(self,instruction["data"]);
postToParent({"cmd" : "func_call" , "scope" : ["logMessage"],"params" : ["Worker establishment done"]});
}else if(instruction["cmd"]==="func_call"){ // Parent page trying to call worker function
try{
var retValue = callFunction(instruction["scope"] , instruction["params"]),
scope = instruction["scope"].reverse(),
params = instruction["params"];
postToParent({"cmd" : "func_call" , "scope" : ["logMessage"],"params" : ["Value for "+scope.join(".")+" for params "+params.join(",")+" = "+retValue]});
}catch(err){
}
}
};

task1.js

(function(w){
var task1 = {};
task1.Controller = {};
task1.Controller.add = function(a,b){
return a+b;
};
w.task1 = task1;
})(self);

task2.js

(function(w){
var task2 = {};
task2.Controller = {};
task2.Controller.multiply = function(a,b){
return a*b;
};
w.task2 = task2;
})(self);

Cheers!!! Hope you find it useful.