Web workers in 3D web applications



From your experience when does it make sense to use web workers in a 3D web app?

For example, when adding interaction in the scene with Ray casting or generating paths using path finding algorithms, could workers be useful or better to keep that on the main thread? Other use cases?


Web workers can do lots of Good if you have a decently large app. It takes a bit of work to figure out how to handle the details. I use a setup where everything runs in workers except rendering and animated models. SharedArrayBuffer as attribute buffers allows you to control the scene from a worker thread without using postMessage which is too slow for reliable rendering.


But it’s only supported by Chrome and FF. Transfering regular buffers doesn’t has a big cost, it’s mainly just passing the pointers.


In context of Game AI programming, we (the YUKA team) made no good experiences with workers. There is a lot of stuff like line of sight tests, path finding or inference logic that could run in a separate thread. But we’ve realized in our performance evaluation tests that the thread management and communication overhead with workers is just too high for these types of unit of work. You would have to merge different tasks into bigger ones so the thread has more work to do. But this approach is just not feasible.

Instead of using workers, we made good experiences by using window.requestIdleCallback(). We build a lightweight layer (YUKA.Task and YUKA.TaskQueue) on top of this API in order to provide a simple task management for the library. We can now wrap certain amount of non time-critical work like pathfinding in tasks and automatically execute them when the browser has some idle time.


Thanks guys for your great feedback!

@Oskasb SharedArrayBuffer seems to be very interesting, but its current very limited support makes me to not use it for now, until all the main browsers will implement the spec.

@Mugen87 If pathfinding is used for player movement, could not be a risk to put the path finding calculation on an idleCallback?

About raycasting, is worker postMessage too slow for this use case (e.g. collisions or path constraints)?


You can define a deadline after your code is definitely executed. Besides, a path finding query normally returns a path which is usually an array of waypoints or edges the AI steers along. So the movement itself (I mean the translation from a point in 3D space to another point) is not affected by path finding. The worst thing that can happen is that the AI moves a bit in the wrong direction. In most cases, this is not noticeable for the player.


Good points! I was thinking more to a point & click adventure, where you can use path finding to build a path of waypoints for the player to navigate around.


Transferable objects can be used as a halfway decent protocol as fallback where SharedArrayBuffer is missing. But that route you need to expect a frame or two of latency so it is a bit harder to live with. You can also use offscreen canvas to move the whole rendering to a worker. This should allow you to run a css based ui in the main thread, but this is speculation on my part as i havnt tried it myself.


I’ve been working on my game for the past 3 or 4 years or so and have rewritten the engine multiple times from scratch due to unforseen performance overhead when running everything in a single (main) thread.

From my experience, running the entire renderer (threejs) in a worker will give you a pretty big performance increase if you’re having a lot of things happening in the background and on the UI-side of things. Remember that your browser does alot of DOM-related tasks on the main thread too whenever a user clicks something or an element’s CSS is updated.

Having threejs in a dedicated worker allows it to keep rendering images while your browser can perform its tasks unhindered on the main thread as well, giving your users a much smoother experience (no microstutters). You’ll have to use requestAnimationFrame from a worker though, which is relatively new.

As for things like pathfinding, I’ve moved this to a separate thread as well because my level geometry is made up of voxels, so its pretty small. You don’t want to ping-pong your entire scene graph over to other threads, thats too much overhead due to serialization. So basically just sending the mutated state of the level to the worker whenever it changes (which isn’t very often), then I simply query for a path using “from” and “to” vectors from the pathfinding thread.

It takes a lot of work to set-up “inter thread communication” properly and effectively, and it can create a lot of overhead if done improperly, but it can most certainly give more benefits than doing everything in a single thread. My engine (or rather framework) uses dependency injection to have “small modules” which can communicate with each other over different threads. Everything is written in Typescript, which makes development so much easier and more comprehensible.

// This runs in a worker context.
@Thread // Mark this class as a "spawnable worker" that can be created using the ThreadFactory.
class PathfinderThread
    @Delegate // mark the method is a delegator for threading.
    public async getPath(from: Vector3, to: Vector3): Promise<Vector3[]>
        const route = [];
        // Route building logic here.
        return route;

// This runs in another thread. In this case, the renderer thread where Threejs lives.
export class Renderer
    @Inject(ThreadFactory) private threadFactory: ThreadFactory;

    private pathFinder: ThreadDelegate<PathFinder>;

        (async() => {
            this.pathFinder = await this.threadFactory.spawn(PathFinder);
            // Fetch the route. The "request" here is the [[ThreadDelegate]] that knows about methods
            // marked with the @Delegate annotation and communicates with them as regular javascript functions.
            // Data (de-)serialization happens automatically based on received types.
            const myRoute = await this.pathFinder.request.getRoute(
                new Vector3(0, 0, 0),
                new Vector3(10, 5, 23)
            console.log(myRoute); // Will show an empty array :p 

tl;dr: Yes, web workers are benificial, but only if done properly and used for the right reasons. The only thing I personally found most benificial is moving the renderer (threejs) to a worker. The other things like AI-logic or game-logic completely depends on what your workers need to know and if the communication between threads is worth the overhead.