But chances are if you have just been doing standard web development there has not been much use for these kinds of things. With the exception of course of service workers and so on. However, let’s consider a highly resource-intensive application like a game or a 3D modeling program. With modern JS game engines like Three.JS and Babylon.JS, you can easily get some models rendering on the screen but what if then you have to perform an expensive calculation like rebuilding a mesh or doing some complicated game logic?
Well if everything is in the main thread this will cause the user to either experience immense slowdowns or even lockups. And this is something we want to try to avoid as much as possible in order to provide a great user experience.
So, this is where Web Workers, Shared Array Buffers, and Transferables come in. If you do not know what those are I will explain each briefly here.
Shared Array Buffers
A Shared Array Buffer is a fixed-length array that can be shared between two separate contexts.
Note: This feature is supported in modern browsers but best used in a Chromium base (for now at least). A while ago it did cause some security vulnerabilities and was taken out of spec but since has been patched and put back.
If you want to see a full list of what all can be transferred here is the MDN docs on transferables: link
The goal in mind is to try to keep the communication between the main thread and the web workers as minimal and as fast as possible.
Example Use Cases
So now I will show each of these things in action in relation to the example I provided above. I am currently developing a voxel engine in TypeScript so I will show how I actually used each of these. I am going to be using TypeScript for these examples but they all can be converted into JS.
First, let’s start with setting up a web worker. Which looks like this:
You could have this in a class or a stand-alone function like this. But you can see that we simply just give the Worker constructor the relative path from the current to the worker script. The second param is an options object. In this example, I enabled modules so we can import other code into the web worker which is really nice.
After we set the worker up next, what we have to do is set up the worker’s onmessage and onerror functions. This is where you will set up communication between the main thread and the worker thread.
Now lets us look at inside the worker:
So, now we have successfully set up communication between two separate threads.
Now that we got our workers set up, what are we going to use them for? Well in this example let’s say we have a worker that generates a game world procedurally and builds a 3D model from it. A 3D model can be created from scratch in a game engine like Three.Js and Babylon.JS (even plain gl if you want to do it yourself) by providing a set of vertices and index pairs for the vertices that specify the faces of the mesh and their orientation. We can also supply UV coordinates to texture the model and calculate its color to do “light baking”. But all of which can be a tremendous amount of data.
Let’s say for any given area 16×16 meters of the game world there could be potentially a model with a quarter of a million or more vertices. Sending all that data through the normal postMessage causes those huge arrays to be serialized and then deserialized on the main thread which would grind the program to halt.
So, what can we do? This is where we can use transferables.
Here is an example of it in action:
As you can see before we send the data to the main thread we create array buffers from the original data arrays. And then on the main thread, it only has to deserialize the data’s intent and the model’s new position To get the arrays back from the array buffers we just make a new TypedArray from the buffer. Make sure that you use the same TypedArray in the worker and main thread otherwise it could cause problems.
Can This Go Faster Though?
Well if you noticed we only created one other process off of main. But what if we could have more? So, in this 3D example let’s imagine that the worker we just created instead of itself calculating all of the mesh data it sends a condensed sort of template to another worker that builds the mesh data and sends that to the main.
This is where we can use something called a MessagePort to connect other workers together. So, in this example, we are going to create an array of workers that will be sent the model’s template data. The number of workers will be calculated by the machine’s thread count which we can get by using “window.navigator.hardwareConcurrency’ (You could probably get away with using half or less but in this example, I am just showing that you can try to use the entire CPU if you want).
But here is what that would look like:
As you can see we create a few instances of the builder worker and when it is time to build a new model we send it just the template. It then constructs the model and sends it to the main thread. So, with this set up you could build as many meshes as you have workers. But all of this stuff is happening more or less concurrently. What about parallelism?
Well, I am going to give a simple example but should give you a good idea of what you can do with a Shared Array Buffer.
The thing you need to note with a Shared Array Buffer is that they are a fixed size and must be declared their size at the start. You define the size by the number of bytes. So, if you need an array of 3 32 bit floats you would need 12 bytes 4 bytes for each number. Then in order to use the array-like, it is an array of 32 bit floats you must create a TypedArray from it that is also a 32BitFloat. And of course, we are using signed bytes so we can go in the negative also.
Another thing to note in this example. I have one thread write and one thread read. You could run into problems if you are having multiple threads read and write at the same time. If you are doing this please check into Atomics: link
It basically ensures thread safety when reading and writing shared data across multiple contexts.
So, a use case for the game example would be keeping track of the player’s position and doing something based on where they moved or where they are looking. What we can do actually is continually load the player’s position into the array with the shared array buffer. And from any other context that we share that buffer too can also access where the player is at. So, let’ see that in action:
In the player class, we are just getting the player’s position and setting that in the shared array. While in the worker we are just reading that data from the array. So, in this way we can react instantaneously and in parallel to the player’s updated position. Pretty cool huh? We don’t have to fuss with postMessage or message events at all.
Other things you could consider are things like web assembly and if you are doing game programming you could maybe move some intensive actions to the GPU like UV animations on a model.