Playing with threads in JavaScript
So, lately I’ve been doing a lot of multi-threading in C. Which has been a lot of fun. But, I also know that there’s a way to do it with JavaScript using workers.
I’ve used service workers in the past to do some caching and pre-emptive reloading. When I used it in the past, it cut load times of our worst pages by half and made other pages feel instantaneous. However, service workers are meant more as a caching layer/custom proxy rather than as a way of doing general computations. It’s definitely not a thread I’d want to clog up in a modern web app given that they make so many network calls.
But, that still doesn’t mean that doing some sort of threading wouldn’t be useful. I’ve had a lot of pages get bogged down because there’s a large computation done in the main thread - a big no-no for anyone familiar with desktop development.
The problem with JavaScript is that it’s a notoriously single-threaded environment where all the code you write is in that main thread - or is it?
Well, surprise surprise, someone on the committee realized only having one thread was a bad idea, so they created something called “workers” which are basically background threads you pass messages to and receive messages from. Really handy.
The model is less the “C/Java” model where memory is shared (with some very limited exceptions) and much more the Erlang/Elixir model of message passing. Though it’s also pretty low level and doesn’t come with a good protocol for “awaiting” an event you send off. That said, it’s not too difficult to create a very crude protocol, and there are some really polished libraries out there.
For my simple use case, I went with just doing a simple protocol where I auto-assign ids, create a promise, and then put that promise’s resolve and reject methods in a map, followed by returning the promise. Then when I send a response back from my background thread, I include the same id as well as the data (or error), and then I lookup the corresponding resolve/reject method and call it to do a dispatch.
The reason for needing a map is simply because we only have a global “onmessage” handler which doesn’t have the context of what we just sent. So, we need a way to tell that handler where the resolver or rejector is so it can forward things along.
Here’s the little snippet of code I use:
let incId = 0
const backgroundThread = new Worker('background-process.js')
const workQueue = {}
backgroundThread.onmessage = e => {
const jobId = e.data.jobId
try {
console.log('Page worker job count', e.data.jobCount)
if (typeof e.data.res !== "undefined") {
workQueue[jobId].resolve(e.data.res)
} else {
workQueue[jobId].reject(e.data.err || 'BAD MESSAGE FORMAT')
}
}
finally {
delete workQueue[jobId]
}
}
function asyncJob(job) {
const jobId = crypto.randomUUID()
const ret = new Promise((resolve, reject) => {
workQueue[jobId] = {
resolve,
reject
}
})
backgroundThread.postMessage({jobId, job})
return ret
}And just like that, we have a really easy way to send jobs to our background thread and get a response back. In our code, it will look like the following:
async function doCalc(n) {
return await asyncJob({expensiveCalculation: n})
}Of course, every protocol has two sides. So, let’s look at the other side: the background thread.
For this, we also have only a global “onmessage” handler. However, we don’t have to coordinate the state with some other context’s data, so we can pretty much do everything in the handler. Here’s my handler code:
let jobCount = 0
onmessage = e => {
++jobCount
const job = e.data.job
const jobId = e.data.jobId
if (job.hasOwnProperty('expensiveCalculation')) {
return sendResponse(jobId, Math.pow(13, job.expensiveCalculation))
}
sendError(jobId, "bad job type")
}
function sendResponse(jobId, res) {
postMessage({jobId, res, jobCount})
}
function sendError(jobId, err) {
postMessage({jobId, err, jobCount})
}This preserves the job id, does the calculation, and sends it back. Not too bad.
There is a demo I have online which shows this off, as well as shared workers (very different from service workers) which I’ll talk more about in another post. To help illustrate the lack of stuttering, I have an animation that plays in the JS loop with an update every frame (it’s just a really small square bouncing animation). There’s also a button that runs everything in the main thread instead of the background thread for compare/contrast. I also use something a little more expensive (fibonacci + factorial of large numbers with BigInt) to make the stutter more noticeable on the main thread example.

