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


 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

Imagining software in a mesh-networked, potentially-disconnected future.

We’re living in an increasingly connected world. Phones synchronise our photo libraries to our cloud provider of choice. Content distribution networks handle edge caching to help us stream our favourite television shows. A message can be... Continue →