18 comments

[ 3.5 ms ] story [ 14.2 ms ] thread
Other than the built-in thread pool task scheduler, how does this differ from Comlink [1]?

1. https://github.com/GoogleChromeLabs/comlink

After having done an initial read on threads.js' docs, I'd say Comlink is missing the entire "observables" functionality. Also, threads.js' API seems less confusing (this is subjective, of course).

Either way, props to OP for the great work!

I've done a few things with workers years ago - and I remember thinking "someone is going to make the socket.io of workers someday, because this is verbose!". I think this delivers!

I've had an idea for a fun little JS game for a while now - seeing this is going to give me an excuse to build it and try workers at the same time.

Thanks for sharing!

(comment deleted)
If all async functions are immediately awaited, there is only one thread of execution - no concurrency.

If they are not awaited, someone has to manage an array/list of promises.

Is there a worker model where the worker can work on a series of tasks?

The event loop will queue these up for you, but it should be possible to build a queuing library over this. What kind of API did you have in mind?
(comment deleted)
> If all async functions are immediately awaited, there is only one thread of execution - no concurrency.

In a Node web server process, though, this isn't really true. If you perform a CPU intensive task on the main event loop, all other incoming requests will pause. Using worker threads instead turns that CPU task into, for all intents and purposes, an async io call, freeing the main event loop to handle concurrent incoming requests.

Since handling CPU intensive tasks is the main downside of a Node web app, I think this library is a great solution for that (I think this library's API is a much easier to use wrapper around native Worker threads).

I’ll be excited for the day when support for worker threads is more widespread.

Tried to use them with Node Canvas a few months ago but it’s not context aware[1] so had to go back to forking processes.

Intriguing to see someone in this thread say they already work with TFjs.

[1] https://github.com/Automattic/node-canvas/issues/1394

This library checks out - I recently used threads.js in combination with a Tensorflow.js integration in a React app to analyze the metadata of WebRTC feeds at production scale.
> Shield sensitive functionality

Can someone explain to me how this is possible? It seems dubious.

Anything that could attack from the main thread would have access to the source and the communication channel with both the input and output of this thread. Technically, the thread would be protected - but that doesn't seem to be the attack vector.

web workers requires a separate source file, and makes you sweat the details of the structured clone algorithm. It feels absurd coming from other languages: so much ceremony!

There is parallel.js, which allows you to just pass your task as a function:

    p.spawn(someFunc, someArg)
This is nice to use, but the implementation is forced to serialize the function via toString, and then eval it in the thread. This prevents the function from capturing anything or calling other functions, which are serious limitations.

It's unfortunate that there's no way to to just "run this function in the background" like so many other languages has. Filip Pizlo explored what it would take: https://webkit.org/blog/7846/concurrent-javascript-it-can-wo... , it's technically doable at least for JSC but is unlikely to ever happen.

Anyways background computation is one of the weakest parts of JavaScript. Happily wasm threads don't have the same limitations and so can take over.

The issue with data processing with workers in both Node and the browser is paying for serialization costs. Until we have rich typed containers & structs on top of raw shared memory arrays (which necessitates manual memory management), all these parallel libraries are highly constrained to domains where the unit of work is very large, and the result is either a side-effect or very small.

I tried to use workers to speed up a JS bundler/compiler system 6 years ago by farming out parsing & analysis work, and getting results back onto the main thread for my naive implementation took more CPU time than the work itself.

Deserialization costs on the main thread is one of the biggest issues we have in the Notion back-end as well - no matter how fast the DB/cache are, we’re throughout limited by JSON.parse (or equivalent). I’m thinking seriously about Arrow because we could keep data in a Dataframe instead of as JS objects, and share freely between threads without copy, but that kind of strategy would require a large codebase rethink. Much more expensive than other languages where a bit of shared memory threading can truly speed up data processing.

tldr; worker libraries are a catch-22: they help a lot for people who don’t need them, and they don’t help much for people who do need them.

I had a pretty clear use-case: running a cpu-hungry ML model at 5-10 FPS without locking a dynamic UI that needed to stay around 60fps.

I tried out all the various worker libraries: thread.js, comlink, etc. What I found is that while these libraries do make it easier to get a worker up and running, they introduce abstractions that make it harder to achieve the goal of using workers in the first place: performance.

The browser APIs for WebWorkers are not difficult to learn and use. But stashing code into a worker doesn’t magically solve performance issues. It’s just the first step.

In my case, I had to optimize the transfer of data to and from the worker. Any abstraction that sits in front of that transfer ends up being at best being an additional and unnecessary cognitive load, and at worst being an obstacle to my original goal of improving performance. While I’m sure these libraries take pains to ensure they don’t do anything that might impact performance, the slightest possibility of a library’s contribution to degraded performance means that your cognitive load now includes ensuring the library is performant too.

It’s analogous to hunting down a tricky bug. Whenever I get a bug that is particularly hard to understand, I end up performing dramatic (albeit temporary) surgery on the codebase to remove anything extraneous to the bug. If I can remove 95% of the code, then I only need to think about 5% of the code. While it is true that the code I removed had nothing to do with the bug, my eyes and brain have an easier time ignoring code that doesn’t exist rather than code that doesn’t matter.

Same for these worker libraries. It’s more code that you have to consider. And if you are using a worker for a good reason — to solve a hard performance issue — you really don’t want to have to consider any more lines of code than absolutely necessary, and ideally you only have to consider lines you’ve written yourself.

I’m not usually in the camp of “write everything yourself”, but leveraging WebWorkers for performance is one of those cases where you are going to get your hands dirty anyway. You might as well embrace it from the start.

Now, if you already understand workers inside and out, then these libraries probably would save you time and be less bug-prone in handling thorny use-cases like workers that run in isometric codebases. Ironically, those who least need a library gain the most from it, while those who most need a library gain the least from it.

Interesting. It seems like the biggest bottleneck in javascript parallelism will always be serialization. Time to learn rust and wasm i guess.
well it depends, my company offloads md5 and uploading files inside a web worker. producing an md5 within the browser for a file is a task that would block the ui, but within a webworker it is really simple, we also upload the file than inside the worker.

it's really simple and offloads lots of stuff from the ui which can than have a nice looking upload spinner. (but we reduce what we transfer to the worker, btw. you can easily send a file handle)