Using Web Workers to run JavaScript in parallel

JavaScript, in the browser, runs in a single thread. This is fine, and works fairly well for most websites. However, when running large, complex tasks or long scripts, it makes the webpage unresponsive.

The best way to speed up these tasks is by running code in separate threads. This has been doable for some time, using ArrayBuffers, but it has considerable memory overhead.

An experimental feature, SharedArrayBuffer, provides a way to effectively share data, using Atomics, between threads.

How do we achieve this? #

Web Workers provide a way to run code in seperate threads. These threads can run parallel to other threads, and ensure that our page doesn’t freeze up while processing.

The downfall of Web Workers is that they can’t access the DOM or variables in the main thread, but it can make XHR requests.

This means that we need to somehow communicate results of the worker threads back to the main thread to be presented to the DOM.

Example time #

I’ve prepared three examples in JSFiddle with benchmarks for each—best used in Chrome.

No parallel code #

Parallel code using ArrayBuffer #

Parallel code using ArrayBuffer & 4 workers #

Parallel code using SharedArrayBuffer & 4 workers #

SharedArrayBuffer only works in Firefox 46 and above, and in Chrome.

[1] This feature is disabled by a preference setting. In about:config, set javascript.options.shared_memory to true.
2] The implementation is under development and needs these runtime flags: --js-flags=--harmony-sharedarraybuffer --enable-blink-feature=SharedArrayBuffer

Observations #

Cloning arrays between more than one worker is difficult #

I kept getting this piece wrong, and it took forever to finally get it to accurately clone the array and return the chunk with the correct offset.

Cloning uses way more memory than expected #

Dealing with a 2MB ArrayBuffer should have only used around 6MBs of memory - but resulted in 10MBs total usage. This may be because 2MBs is used with the initial ArrayBuffer creation, cloning costs another 2MBs, copying the clone to the worker costs another 2MBs, the worker clones the copy costing yet another 2MBs and then the worker moves the copy back to the main thread at a low low price of another 2MBs. Totalling 10MBs of stuff.

If you want to observe the weirdness of the cloning - check out the call stack.

postMessage uses less memory because we don’t have to clone #

postMessage is a “transfer list” and can hold a list of Transferable objects that will just be transferred instead of cloned. The idea behind this is to use less memory and to not clone things pointlessly.

When running the test, about a maximum of 4MBs memory was used.

Using too many workers will actually be slower #

Keep in mind that most systems have a certain number of cores / threads. If you spawn more workers than there are threads, you’ll actually slow down the workload as it will need to queue the workers.

This doesn’t test against race conditions #

By breaking up the ArrayBuffer with an offset, each worker has its own part of the “memory” to write to. More complex structures could easily overwrite the same part of the memory - oops.

SharedArrayBuffer works with Atomic operations (see Mozilla’s proposal) to resolve this issue.

To conclude #

JavaScript is single-threaded and freezes up the DOM when running complex operations. You can dodge this using WebWorkers.

SharedArrayBuffer and Atomics provide an exciting future for multiple threads and shared memory in the browser.


Now read this

Multi-node docker deployments with persistent storage

A trait of Docker is that it doesn’t persist state. This is usually great when working with worker applications that don’t need to store and share data, however in the case of databases or even small WordPress instances, persisting state... Continue →