When to use web workers
dassur.ma
dassur.ma
But it seems to me that Webworkers were designed with an extremely narrow use case in mind. The restrictions placed on them make them essentially useless for me. To do anything ever remotely useful, I would need to duplicate my entire database in web workers, and then communicate with them through a thin straw. Database updates would need to be performed both in the main app database and in web worker threads.
The way they are restricted, I just can't figure out how to make any meaningful use of them.
If the app state is stored in a single atom, I would use localForage[1] (available as a cljsjs package[2]) to keep it in sync between web workers and the main thread. It would also make the app accessible offline.
I would say Animation Worklets are the workers that will make an impact on delivering multithreaded UIs in web.
http://www.pocketjavascript.com/blog/2015/11/23/introducing-...
That being said, I think even smaller-scale things like state management and hitting APIs should be moved to workers. It all increases resilience and buys headroom for low-end phones.
That being said, I’ve been a web developer for over 5 years and have never actually decided to move tasks to web workers; it’s about time I change that.
I also find it weird when devs complain about the lack of shared state and how it’s some how a limitation. Messaging passing architectures haven proven key for modelling concurrency in many powerful languages and frameworks like Erlang/Elixir and Go channels to name a few.
Anyways thanks for writing this up. Proper breakdown and grouping will have unseen benefits for sure, and in this case, very seen :)
Because it requires a separated .js file to instantiate, bundlers like webpack tend to introduce weird and non-standard way to work with [1].
It is usually fine when you use it your own project since you are going to stick with a bundler you choose anyway, but the worst things happens when you try to publish a JS library that uses a web worker internally. You cannot just publish the source because bundlers won't recognize require() and imports syntax in the web worker source code out of the box.
There is a ES standard:
new Worker("worker.js", { type: "module" });
but no browsers implementations yet and some people aren't happy with this because it is not async [2].[1]: https://github.com/webpack-contrib/worker-loader/blob/master...
[2]: https://bugs.chromium.org/p/chromium/issues/detail?id=680046
Since I mostly use rollup, I ended up writing a plugin[1] that allows you to pretend that modules are in workers. It compiles down to AMD under the hood and uses a very minimalistic worker.
new Worker(function(params) {
... compute heavy stuff here ...
});Domenic and I have been working on a proposal[1] called Blöcks to introduce transferable functions to JavaScript.
In the meantime, I wrote Clooney[2] ontop of Comlink[3] that gives you almost that.
[1]: https://github.com/domenic/proposal-blocks
It seems like you should stick with function invocation syntax:
`worker = (endpoint) => {| block |}`
`worker(endpont) // does what you want`
with the difference that the result isn't a closure. As a bonus this syntax would let you write 'pure functions' even if you weren't working with workers. Perhaps the worker version then is `worker = async (endpoint => {| block... |}`.
Although considering the precedence for `async function` maybe the way to do this ought to be `pure function`.
https://github.com/franciscop/uwork
const findPi = uwork((iterations = 10000) => {
let inside = 0;
for (var i = 0; i < iterations; i++) {
let x = Math.random(), y = Math.random();
if (x * x + y * y <= 1) inside++;
}
return 4 * inside / iterations;
});
// Run this inside an async context:
const pi = await findPi(200000000); new Worker(function(params) {
... compute heavy stuff here ...
});
In 2015 I worked on a framework that functioned like this as a personal project. It was even context-aware and spun itself up differently depending on the environment it was in; workers and shared workers loaded the same .js file that the UI thread did. As such, it chose speed over memory use seeing that the code was effectively pre-loaded everywhere.
It was never completed, and the reasons for that were:
1. SharedArrayBuffer hadn't landed yet. Transfer cost of computable data between workers was high, and transferables had shortcomings of their own.
2. Closures were disallowed, and AST transformations were one possible solution. It seemed like a very bad idea to go down that road.
3. Back then the differences in API behavior between even Chrome and Firefox were terrible to deal with. The APIs were heavily neglected across the board.
These days, I don't see the need. If you need the performance, WASM is a better choice anyways and depending on what you're doing it can use SharedArrayBuffer much more efficiently via way of something like pthreads.
I think you misunderstand the concept of WASM. WASM does not have its own thread, it blocks the main thread and runs in the same context as JS. WASM still must spawn a Web Worker to emulate multithreading in a browser.
What I was saying is if you're in the WASM toolchain/ecosystem, something like pthreads would already be implemented by Emscripten[0]. In other words, spinning up web workers is handled for you seamlessly. It's all dealt with at a very low level of abstraction. You're literally using the SharedArrayBuffer as memory.
Contrast that to JS, and you have to serialize/deserialize types before they can even be stored in SharedArrayBuffer. It's costly and far from seamless.
As an aside, it's unfortunate SharedArrayBuffer was hit so hard by Spectre. The poor standard is cursed or something.
Workers, on the other hand, are available everywhere. And serialization seems to be far less costly than most people think. In the apps that I have written that make use of a off-main-thread architecture, structured clone has not been my bottle-neck.
If you're working within a render loop it does hurt, but I agree it isn't an issue for a typical use case. I think what detracts more is the ergonomics of the serialize/deserialize operations. Those are just largely seamless on the emscripten side. On the other hand, a full wasm/emscripten toolchain in your project arguably comes with its own ergonomic cost.
func hello(value string) {
// compute heavy stuff
}
To run it sync hello("World")
To run it async, just add 'go' go hello("World")
IMO when it has generics, it'll be the best language I have worked with.Is there any other reason why you didn't like worker-loader?
I experimented with it and found it pretty straightforward. For anyone who wants to try it, you can even get it working from a create-react-app project with the webpack inline loader syntax without ejecting:
/* eslint import/no-webpack-loader-syntax: "warn" */
import Worker from 'worker-loader!./Worker.js';
My use case was to run tensorflow.js in the background. This actually mostly worked out of the box with only some awkwardness with serializing images as messages. The only thing that didn't work out was that you don't have access to webgl there which defeated my original use case (since there's no point in offloading computation if that computational is going to be several orders slower in the background).Besides, I personally don't feel good with Webpack's do-everything-with-import approach. It is straightforward of course, but is ES6 import statement supposed be used like this?
function workerFn() { ...Do worker stuff }
let worker = new Worker(URL.createObjectURL(new Blob(['('+workerFn.toString()+')()'])));
Though this hack works but it still have problems to be addressed:1. It still does not resolve import/require(). This introduces another hack that dumping output of a bundler to workerFn.
2. It will make it much harder to debug since you create worker out of data, not source code.
I had some promising (at the very least interesting) results with react-native-dom which runs all of React's reconciliation & your own business logic in a web worker: https://rndom-movie-demo.now.sh. I'll fully admit there's a lot more exploration/experimentation left to be done in this space though.
But it's just not possible for Safari... you either have to make your code much more complicated, or add some hack to prevent users from opening your app in multiple tabs at the same time :(
I realize we need more browser diversity and not less, but at the same time we should encourage all participants to keep up so that the burden doesn't fall on smaller players.
So if someone asked her, she would just switched the windows.
they're just as likely to use a different site instead.
just as likely, or more likely?Short of the experimental OffscreenCanvas API[1], I don't see any way to avoid the structured clone memory tie-ups when communicating from worker to dom. Are there any other patterns or approaches that can help limit the memory consumption when employing workers?
[1] https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCa...
That being said, in all the off-main-thread apps I have written so far, the cost of structured cloning was pretty much irrelevant.
Maybe if no one else is taking advantage it is time to simply throw them out.
As an example, emscripten uses WebWorkers and SharedArrayBuffers to implement multithreading (unfortunately or not, Chrome is the only browser that supports SharedArrayBuffer, others have disabled it).
It would be much more helpful to the cause if you could point to some actually used app out there principally relying on WebWorkers. Until then, it's been a lot of pain, zero gain.
in addition to the render blocking issue brought up by this article, there are a lot of applications for cpu-bound web apps (eg. anything requiring machine learning or computer vision), but in my experience web workers are not very reliable for this purpose.
There is also some niche use as a component of sandboxing: https://gist.github.com/pfrazee/8949363
Your point is well-made though. Very few kinds of webapps are CPU-bound, graphing calculators being notable as an exception (and even then, Desmos made do with the main thread before Web Workers were usable). And while lots of webapps could use plugins, browsers could provide an API for sandboxing code that runs in the main thread, or with a GIL.
The reality is, though, the cat is out of the bag. WHATWG is committed to making the Web into a fully-featured platform for WORA apps with all the same capabilities as a desktop app, and multithreading is one of those capabilities. Even if the next Spectre/Meltdown dooms Web Workers in their current incarnation, browsers will run workers in Docker containers if they have to.
Not everybody is using web workers for that, but that could address both issues to a degree.
In conjunction with this, I’ve long been thinking about a new style of isomorphic rendering: where instead of running on the server and in the DOM, you run the server code on the server and in a service worker on the client side. (Aside: Svelte has been a major inspiration in the last few months; learn from it that SSR doesn’t need to mean VDOM: if you’re a compiler you can take different approaches.) Cloudflare Workers, when it came along, brought clarity to what I had been thinking at about that time, that it might work to have a pure API backend, and an HTML renderer that speaks to that API and can run either on the server or locally in a service worker on the client: truly use the same interface for both. (To clarify: this approach does not preclude additional client-side scripting for interactivity; but it would lend itself to a light hand on client-side interactivity.)
Now to the relevant point here: a couple of weeks ago I started toying with the idea of doing just about all of the client-side scripting (I mean the stuff for interactivity without full page loads) in workers, leaving the work done on the UI thread being purely applying UI changes that were even calculated in a worker, and event dispatch. This might be able to be slotted into the service worker in some way, or it might need to be another worker.
The essence of what I have in mind is that rendering in the worker would, instead of applying changes to the DOM, emit a byte code (I’ve been looking a very little into Glimmer.js’s), which can then be fairly efficiently passed back to the UI thread through one ArrayBuffer or SharedArrayBuffer, to be applied by a small unit of code (probably JS rather than Rust) to the DOM. In the last few days I’ve been playing with events, and adding the listeners on the document root and doing dispatch manually, through components more than through elements, in a way that lets the framework deal with hierarchical ownership (very Rusty, allowing you to skip GC/RC types) rather than something closer to the ECS style (what is mostly done for UI things in Rust), and I think the approach has promise. (Apart from the hierarchical ownership aspect, this is basically what our framework Overture that we use for FastMail does—and I should clarify at this point that these experiments of mine are personal and nothing whatsoever to do with FastMail, where we have no workers at all, like almost all sites—though I wrote one this very day that might be deployed in the coming week).
Events would be serialised on the UI thread and passed through to the worker. All events would thus need to be passive (i.e. no preventDefault()); though there will doubtless need to be some alternative channel for events that need to preventDefault, most notably clicking on links that should route instead, and form submit; I’m not sure how that will work.
Some parts of this I’ve written code for, to experiment with ideas, but especially the parts involved with workers have almost entirely been thought experiments. I’ve been thinking about the approaches SwiftUI takes too. Lots of interesting stuff to learn from it as well.
I’ve written all of this purely for thinking about. Maybe others will find it interesting. (I shan’t be able to respond to anyone that replies for the best part of a day.)
Naively I would expect that to be part of the rendered DOM("<a prevent-default='click'>"), or otherwise in a declarative datastructure available to the UI thread. Are there many events that need conditional preventDefault, such that the JS handling code would grow nontrivial?
Either way, I can’t think of anything where you need fully conditional preventDefault. Definitely something closer to declarative than imperative is good for these sorts of things.
A tiny typo in the second-to-last section: specture -> spectrum
Thanks again!
FYI: AsyncTask and MicroTask are equivalent, and so are my task() function and your OnMessageTask
I expaned my tests a bit and your version does indeed queue a task and not a micro task. I did find it to be somewhat less reliable than setTimeout, is there a particular reason why you used this scheme instead of a setTimeout based solution to queue tasks? Why did you choose this method in particular to queue tasks?
Also, AsyncTask and MicroTask are not fully equivalent. MicroTask is equivalent to a bare `await 0;`, but because AsyncTask wraps that in another async function it will queue a second micro task when the first one resolves, and resolve the promise when the second micro task resolves :)
One final question would be why you have this uid system with attaching and removing event listeners? I think MessageChannels are order preserving, so you can do with a single EventListener that consumes events off a queue, or am I missing something?
``` const { port1, port2 } = new MessageChannel(); const taskQueue = []; port2.start(); port2.addEventListener("message", () => { taskQueue.shift()(); });
const task = () => { return new Promise(resolve => { taskQueue.push(resolve); port1.postMessage(0); }); }
```
When you say, “this site”, do you mean my blog? Because that one’s fully static using 11ty.
So, your understanding is basically correct.
(I'm the tech lead for Cloudflare Workers.)
Why is static not better?
What are the edges?
The article is mostly aimed at use-cases where some sort of logic in JavaScript is required.
I am saying that we should be using web workers to keep the main thread free. That is completely orthogonal to good static design, proper bundling, right caching headers, code splitting, asset hashing etc etc.
Since Wasm is synchronous, I’d say it should almost always be run in workers except if the module needs access to some main-thread-only API.
Outside of that edge case the best solution is usually to use JS to sprinkle functionality not doing heavy lifting.
If you have a phone from 2014, something's gonna jank. It doesn't mean you're excluded. It means you'll have to live with jank. You'll get over it.
Secondly, how does it prevent you from using a website? Sure, jank is annoying, but it doesn't entirely prevent you from using the site. Perhaps you give up on the site, but that's up to you.
I challenge you to run a browser with no ad-blocker installed for one week, then block JavaScript (e.g. with NoScript; or with uMatrix and block scripts; or by setting javascript.enabled to false in Firefox).
Your eyes will be opened at just how much JS is run and how much it slows things down.
> What even is the use-case?
Mostly analytics, ad-serving, privacy invasion and laziness/inefficient coding.
> how does it prevent you from using a website?
When it’s bad enough (which it often is, on slower mobile devices), you reach the point where you can’t perform tasks in a timely fashion. I chose the word “prevents” deliberately, because that is how it ends up.
I was looking for an example of a "bad website" that I could profile, not to see how disabling Javascript makes things faster. Of course it does make things faster. It also breaks everything.
> Mostly analytics, ad-serving, privacy invasion and laziness/inefficient coding.
I remain unconvinced that most any of this could be moved out to a web worker. It sounds more like a "death by a thousand paper cuts".
> When it’s bad enough (which it often is, on slower mobile devices), you reach the point where you can’t perform tasks in a timely fashion.
I do use slower mobile devices for testing other websites, I have yet to come across one site that's absolutely unusable. Granted, I didn't test the whole WWW.
I'll ask you again: Show me one example of such a site, where I can fire up the profiler and observe that it's really the Javascript code and not the DOM or CSS/Layout reflows that's causing the performance issues. Only then can we start figuring out if Web Workers might help - which often they can't, because they are very limited.