Idiomatic React Web Workers: Migrating off the main-thread.

Jay Kariesch
5 min readMar 6, 2020
Photo by Johannes Plenio from Pexels

In modern web development our motivation to create performant software pushes us in complicated directions. Tree shaking, dynamic loading, chunking, memoizing — you get the idea. If you listen to The Web Platform Podcast, you’ve likely heard musings about Javascript tool chains and optimization strategies— and you may have even caught the inspiration for this article, Off the Main Thread. In this article I’ll detail a tiny, partial Off the Main Thread architecture proof-of-concept, designed for usage parity with common hook patterns.

…off-main-thread architectures increase resilience against unexpectedly large or long tasks.

- Surma — When should you be using Web Workers?

Concurrency

Javascript isn’t known for concurrency. In fact, depending on who you ask, it’s really just a hipster language and this is the only explanation you need. But the reality is, concurrency has been available in the browser for more than ten years — in fact, it was introduced at the same time as native JSON support.

Conceptualizing

Deferring processes to threads.

The chart above demonstrates parallelism, where processes that usually run on the main thread on load are deferred to a worker where they can execute in parallel and deliver values back to the main thread when complete. This unblocks the main thread, allowing reconciliation to run unimpeded.

Since DOM API’s are inaccessible within a web worker, non-DOM-contingent and potentially thread-blocking scripts (especially for initial load) will be deferred to improve response, enhance the time to First Meaningful Paint (FMP), achieve snappy client-side page loads, and reduce the time to next frame for an optimal user experience.

Here’s a side-by-side of a before-and-after from a proof-of-concept or rather, a top-by-bottom:

Page load without workers.
Initial page load with workers.

The pages profiled above run two processes (beyond React):

  • A for loop that iterates over a length of 1000000000.
  • A request to https://jsonplaceholder.typicode.com/ to retrieve data, where each entity is iterated over with each body and title string truncated.

The loop is obviously very intensive with heavy blockage, which isn’t quite a one-to-one representation with how most sites load (I hope), but for code-brevity, I felt it was valuable in demonstrating the impact of process deference.

The first profile demonstrates the main thread being blocked for 2000ms, after which the First Meaningful Paint occurs. The second profile moves the massive loop to a worker, along with a fetch + data transformation, and the FMP occurs at around 600ms.

You can check out the demo yourself here.

Pseudo-coding

As the article title implies, the goal is to create an implementation that is in the flavor of React, meaning this proof of concept will use hooks — useWorker and useWorkerCallback . The goal is to create syntactic sugar on top of the web worker API that could be treated as if it were akin to useMemo or useCallback , sans a watcher array. That is, consumers can simply pass in a callback, and the hook spits out publish (ahem, postMessage) method to call the worker, a data property, as well as loading and error properties, too.

The vision:

Abstracting Away Worker “self”

To get started, the most obvious question is, can we inline web workers?

Totally.

Secondly, can we simulate returning a value in a familiar React kinda way?

Yep! But first let’s take a peek at the createWorker function:

In the example above, a callback is passed into createWorker , which is then passed into a string literal (which represents the worker function) and converted to a string.

Then the value sent to the worker is passed into the callback, and the callback is wrapped in the worker’s postMessage method to immediately send the results back.

The string literal is transformed into a blob and the worker is instantiated with.

This implementation eliminates the need to think about what occurs within the worker, and as we move forward, we’ll attempt to further simplify the mental model.

Creating a Hook for Synchronous Data

Creating the hook is fairly simple. The initial overhead is in understanding web workers and tailoring an inline worker’s internals to meet our needs. Below is a pen demonstrating how to create a basic useWorker hook, incorporating what we’ve already covered.

The code is pretty simple, and it’s mostly abstracting away the remaining bits of the worker API and leaning on useEffect and useState to instantiate and update.

const setup = () => {
const worker = createWorker(fn)
cache[key] = worker
if (!(cache[key] instanceof Worker)) {
throw new Error('Could not instantiate worker.')
}
/*
This is where the remaining abstraction occurs.
Once the listeners are attached, React takes
care of the rest.
*/
cache[key].onmessage = onMessage
cache[key].onerror = onError
}

Creating a Hook for Capturing Data within a Promise

The distinction between the two is subtle, and rests within createWorker . I’ve also modified the component that’s being rendered to demonstrate usage,

The magic here occurs in the string literal inside of createWorker .

`self.onmessage = function({data}) { 
var func = ${fn.toString()};
var dispatch = (data) => self.postMessage(data); func(data, dispatch)
}`

Instead of executing fn inside of postMessage , we’re exposing postMessage to our callback, allowing the consumer to use it to return data from within a promise.

To verify whether the worker is running, pop open the network tab in devtools and click “other”:

Limitations

There are limitations to this implementation:

  • If babel polyfills any methods used inside of a worker callback, the worker will break, as it doesn’t have access to the object babel injects during transpilation since it’s in a separate global scope.
  • Dependencies cannot be used within a callback (like lodash, etc). Unless a loader or plugin is used to bundle each worker with its own dependencies, Webpack (or whatever the flavor) will bundle a worker with references to the main thread, that are outside of the worker’s global scope.
  • Out-of-the-box, it doesn’t support classes.

If you’re skimming and missed it, here’s a link to a Github repo with a proof-of-concept I cooked up over the weekend (which has additional features):

Thanks for reading, and if you found this interesting or insightful, hit that clap button!

--

--