WebAssembly: Web Workers
In this article, we conintue our look at WebAssembly, by learning to leverage WebAssembly modules in a multithreaded web application.
Join the DZone community and get the full member experience.
Join For FreeThis is a continuation of a series of articles exploring how we can build and work with WebAssembly modules using Emscripten. The previous articles are not required reading to understand what we're going to cover today but, if you're curious, you can find them here:
- An Introduction to WebAssembly (this article uses Emscripten's helper methods to communicate between JavaScript and the module)
- Using Emscripten to Create a Bare-Bones Module
- Calling Into JavaScript From Bare-Bones C Code
- Caching to HTML5 IndexedDB
Today we're going to continue using a bare-bones WebAssembly module (no Emscripten built-in helper methods) just to keep things as clear as possible as we examine HTML5 Web Workers.
Web Workers allow JavaScript code to run in a separate thread from the browser window's UI thread. This allows long-running scripts to do their processing without interfering with the responsiveness of the browser's UI.
Because Web Workers are running in a separate thread from the UI, the worker threads have no access to the DOM or other UI functionality. In addition, Web Workers are not intended to be used in large numbers and are expected to be long-lived since they have a high start-up performance cost and high memory cost per worker instance.
WebAssembly modules can be loaded in either the UI thread or in a Web Worker. You would then take the compiled module and pass that to another thread, or even another window, by using the postMessage method.
Since you will only be passing the compiled module to the other thread, you can call WebAssembly.compile rather than WebAssembly.instantiate to get just the module.
The following is some code that will request the wasm file from the server, compile it into a module, and then pass the compiled module to a Web Worker:
fetch("test.wasm").then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.compile(bytes)
).then(WasmModule =>
g_WebWorker.postMessage(WasmModule)
);
The code above will also work in a Web Worker if you wanted to do this in reverse (load and compile the module in a Web Worker and then pass the module to the UI thread). The only difference would be that you would use self.postMessage as opposed to g_WebWorker.postMessge in the example above.
The following is some example code that creates a Web Worker in the UI thread, loads the wasm file, and then passes the compiled module to the Web Worker:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<input type="button" value="Test" onclick="OnClickTest();" />
<script type="text/javascript">
// Create a Web Worker (separate thread) that we'll pass the WebAssembly module to.
var g_WebWorker = new Worker("WebWorker.js");
g_WebWorker.onerror = function (evt) { console.log(`Error from Web Worker: ${evt.message}`); }
g_WebWorker.onmessage = function (evt) { alert(`Message from the Web Worker:\n\n ${evt.data}`); }
// Request the wasm file from the server and compile it...(Typically we would call
// 'WebAssembly.instantiate' which compiles and instantiates the module. In this
// case, however, we just want the compiled module which will be passed to the Web
// Worker. The Web Worker will be responsible for instantiating the module.)
fetch("test.wasm").then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.compile(bytes)
).then(WasmModule =>
g_WebWorker.postMessage({ "MessagePurpose": "CompiledModule", "WasmModule": WasmModule })
);
function OnClickTest() {
// Ask the Web Worker to add two values
g_WebWorker.postMessage({ "MessagePurpose": "AddValues", "Val1": 1, "Val2": 2 });
}
</script>
</body>
</html>
The following is the content of our WebWorker.js file:
var g_importObject = {
'env': {
'memoryBase': 0,
'tableBase': 0,
'memory': new WebAssembly.Memory({ initial: 256 }),
'table': new WebAssembly.Table({ initial: 0, element: 'anyfunc' })
}
};
// The WebAssembly module instance that we'll be working with
var g_objInstance = null;
// Listen for messages from the main thread. Because all messages to this thread come through
// this method, we need a way to know what is being asked of us which is why we included the
// MessagePurpose property.
self.onmessage = function (evt) {
// If we've been asked to call the module's Add method then...
var objData = evt.data;
var sMessagePurpose = objData.MessagePurpose;
if (sMessagePurpose === "AddValues") {
// Call the add method in the WebAssembly module and pass the result back to the main thread
var iResult = g_objInstance.exports._add(objData.Val1, objData.Val2);
self.postMessage(`This is the Web Worker...The result of ${objData.Val1.toString()} + ${objData.Val2.toString()} is ${iResult.toString()}.`);
} // If we've been passed a compiled WebAssembly module then...
else if (sMessagePurpose === "CompiledModule") {
// NOTE: Unlike when we pass in the bytes to instantiate, we don't have a separate 'instance'
// and 'modules' object returned in this case since we started out with the module object.
// We're only passed back the instance in this case.
WebAssembly.instantiate(objData.WasmModule, g_importObject).then(instance =>
g_objInstance = instance // Hold onto the module's instance so that we can reuse it
);
}
}
The following is the C code as well as the command line needed to turn the code into a WebAssembly module for today's article:
int add(int x, int y){ return x + y; }
emcc test.c -s WASM=1 -s SIDE_MODULE=1 -O1 -o test.wasm
Inline Web Workers
While reading about using Web Workers with WebAssembly modules, I ran across one comment about how this adds yet another network request (one for the wasm file and now another for the Web Worker's JavaScript file).
The following may not work for every situation, especially if the Web Worker's code is large or complex, but there is a way to create an inline Web Worker by using a Blob object.
You can pass a Blob object a string of JavaScript but I found that using a literal string was difficult to work with and that having a section in the HTML felt more natural which is why we're using a custom Script tag in the sample code below.
The following is an example of how you can create an inline Web Worker:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<input type="button" value="Test" onclick="OnClickTest();" />
<script id="scriptWorker" type="javascript/worker">
var g_importObject = {
'env': {
'memoryBase': 0,
'tableBase': 0,
'memory': new WebAssembly.Memory({ initial: 256 }),
'table': new WebAssembly.Table({ initial: 0, element: 'anyfunc' })
}
};
// The WebAssembly module instance that we'll be working with
var g_objInstance = null;
// Listen for messages from the main thread. Because all messages to this thread come
// through this method, we need a way to know what is being asked of us which is why
// we included the MessagePurpose property.
self.onmessage = function (evt) {
// If we've been asked to call the module's Add method then...
var objData = evt.data;
var sMessagePurpose = objData.MessagePurpose;
if (sMessagePurpose === "AddValues") {
// Call the add method in the WebAssembly module and pass the result back to the
// main thread
var iResult = g_objInstance.exports._add(objData.Val1, objData.Val2);
self.postMessage(`This is the Web Worker...The result of ${objData.Val1.toString()} + ${objData.Val2.toString()} is ${iResult.toString()}.`);
}// If we've been passed a compiled WebAssembly module then...
else if (sMessagePurpose === "CompiledModule") {
// NOTE: Unlike when we pass in the bytes to instantiate, we don't have a
// separate 'instance' and 'modules' object returned in this case since we
// started out with the module object. We're only passed back the instance in
// this case.
WebAssembly.instantiate(objData.WasmModule, g_importObject).then(instance =>
g_objInstance = instance // Hold onto the module's instance so that we can reuse it
);
}
}
</script>
<script type="text/javascript">
// Load the text from our special Script tag into a Blob and then grab the URI from
// the blob
var bInlineWorker = new Blob([document.getElementById("scriptWorker").textContent]);
var sBlobURL = window.URL.createObjectURL(bInlineWorker);
// Create a Web Worker (separate thread) that we'll pass the WebAssembly module to.
var g_WebWorker = new Worker(sBlobURL);
g_WebWorker.onerror = function (evt) { console.log(`Error from Web Worker: ${evt.message}`); }
g_WebWorker.onmessage = function (evt) { alert(`Message from the Web Worker:\n\n ${evt.data}`); }
// Request the wasm file from the server and compile it...(Typically we would call
// 'WebAssembly.instantiate' which compiles and instantiates the module. In this
// case, however, we just want the compiled module which will be passed to the Web
// Worker. The Web Worker will be responsible for instantiating the module.)
fetch("test.wasm").then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.compile(bytes)
).then(WasmModule =>
g_WebWorker.postMessage({ "MessagePurpose": "CompiledModule", "WasmModule": WasmModule })
);
function OnClickTest() {
// Ask the Web Worker to add two values
g_WebWorker.postMessage({ "MessagePurpose": "AddValues", "Val1": 1, "Val2": 2 });
}
</script>
</body>
</html>
Summary
One nice thing about Web Workers is that they also have access to IndexedDB which means the worker thread can handle caching too if you wish to work with WebAssembly modules entirely from the worker thread.
We didn't dig into caching a WebAssembly module in this article but, if you're interested, you can check out our previous article: WebAssembly - Caching to HTML5 IndexedDB
The focus of today's article with Web Workers was just around what was needed when working with WebAssembly modules. If you'd like to know more about Web Workers, I have several articles that may be of interest:
- An Introduction to HTML 5 Web Workers
- A Deeper Look at HTML 5 Web Workers
- A DZone Refcard on HTML5 Web Workers (covers what's in the two articles above but also introduces additional topics like Inline Web Workers)
Published at DZone with permission of Gerard Gallant, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments