What problem are we trying to solve?
Main Thread and Web Workers
However, if there is a computationally-intensive and/or long-lasting operation that needs to be performed, running this on the main thread is not preferable because it can block other UI updates and give the user a slow experience on the web page. One way to get around this problem is to delegate that operation to a Web Worker. A web worker runs on a separate thread in the background and allows us to prevent the UI from slowing down or freezing.
To use a web worker, a Worker object needs to be instantiated from the JS code running in the main thread (or another worker for that matter, if sub-workers are used). The necessary context can be passed to this worker by using the postMessage() function.
Within the worker’s code, the onmessage() function is used to receive communication from the main thread. The worker can then go off on its own and run its operations independent of the main thread. Whenever it is ready to send data back to the main thread, it uses the postMessage() function from within its own code. The main thread can stop the worker at any time by calling its terminate() function.
Since a web worker runs on an independent thread, it does not have direct access to non-thread safe components or the DOM. So all the necessary UI updates must still be done by the main thread once it receives data back from the worker.
Note that a web worker has a high start-up performance cost and high memory cost per instance, so it is not intended to be used in large numbers or just for a short duration.
Another important thing to keep in mind is that older versions of certain browsers do not support web workers. So it is worthwhile to do a check like “if (window.Worker)” before using them.
Using WebAssembly in a Web Worker
WebAssembly is a relatively new concept, whereby near-native performance can be achieved in a web application by means of an assembly-like language that can be compiled from code written in familiar languages such as C and C++. It is particularly suitable for heavy computations such as decoding a video stream. It is desirable for our use case to be able to invoke WebAssembly code and the functions therein from a web worker.
There are plenty of examples online that explain how a WebAssembly Module (WASM) can be imported into the main thread and then passed into the worker as a module through the postMessage() call. Here are a few such examples:
- Loading and running WebAssemblyCode
- WebAssembly: Web Workers
- WebAssembly, Web Workers, and Texas Holdem
Let’s see what happens in action
We have a simple dummy UI with three given async actions: Fetching data in a web worker, Lazy loading of WASM and asynchronous running of the WebAssembly code in a web worker.
Here is how the whole webpage’s HTML code look like:
We have only 3 functions in this script. All three is calling from the HTML code. They’re: basicFetch(), loadWasm() and runWasm();
So, here are they:
When the user clicks on the “Fetch” button, the function basicFetch() is invoked. In this function, a new web worker is dispatched, wherein an example JSON received and returned to the main thread, which then shows it on the page.
The worker’s code is as follows:
Alternatively, if the user clicks on the “Load WASM in Web Worker” button, the function loadWasm() in main.js is called. In this function, a new worker is instantiated and a message is sent to it to load the JS glue code for a trivial WebAssembly module called “our-c-script”.
The user can then tap on the button “Trigger WASM in Web Worker”, which invokes the function triggerWasm() in main.js, which subsequently indicates to the worker to call a couple of the functions defined in the WebAssembly module.
The function _multipleByThree(3) multiplies the argument by 3 and prints the output to the browser console. In the end, the page looks like below:
Here is what the original C code behind the WASM module looks like: