
When to use web workers - tiniuclx
https://dassur.ma/things/when-workers/
======
jwr
I have a large ClojureScript app
([https://partsbox.io/](https://partsbox.io/)) and I really, really wanted to
use WebWorkers. I wanted to run larger tasks like indexing for search, or
pricing calculations in WebWorkers.

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.

~~~
dassurma
IndexedDB is shared between workers and main thread, in case that helps.

~~~
jwr
Indeed, and it is something I'm looking at. But at the moment my database is
really native Clojure data structures, which has fantastic advantages. I would
need to start storing them in a "traditional" database, introducing additional
layers, serialization, overhead and bugs.

~~~
krn
> I would need to start storing them in a "traditional" database, introducing
> additional layers, serialization, overhead and bugs.

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.

[1]
[https://github.com/localForage/localForage](https://github.com/localForage/localForage)

[2]
[https://clojars.org/cljsjs/localforage](https://clojars.org/cljsjs/localforage)

------
msoad
As a UI developer i want to move scrolling and style updates out of main
thread. Every WebWorker tutorial is taking about how a “computationally heavy
task” can go out of main thread. But rarely anyone does computation in the
client.

I would say Animation Worklets are the workers that will make an impact on
delivering multithreaded UIs in web.

[https://www.chromestatus.com/feature/5762982487261184](https://www.chromestatus.com/feature/5762982487261184)

~~~
eddiezane
If you haven't seen this before it might interest you. Nolan worked on
offloading most of the work to threads to achieve 60 fps on mobile.

[http://www.pocketjavascript.com/blog/2015/11/23/introducing-...](http://www.pocketjavascript.com/blog/2015/11/23/introducing-
pokedex-org)

~~~
chii
That's pretty cool!

------
kbumsik
My experience with Web Workers was terrible. It's not because of the Web
Workers itself but there is no proper standard way to use modules inside of
the worker.

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...](https://github.com/webpack-contrib/worker-
loader/blob/master/README.md#config)

[2]:
[https://bugs.chromium.org/p/chromium/issues/detail?id=680046](https://bugs.chromium.org/p/chromium/issues/detail?id=680046)

~~~
bufferoverflow
Can someone explain why the ES standard doesn't have something like this:

    
    
        new Worker(function(params) {
        ... compute heavy stuff here ...
        });

~~~
dassurma
The problem is mostly in that all these functions are closures, and scope
can’t easily be transferred.

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](https://github.com/domenic/proposal-blocks)

[2]:
[https://github.com/GoogleChromeLabs/clooney](https://github.com/GoogleChromeLabs/clooney)

[3]:
[https://github.com/GoogleChromeLabs/comlink](https://github.com/GoogleChromeLabs/comlink)

~~~
ajkjk
Oo, I'm really interested that you're working on that, although I don't love
the syntax (particularly `worker<endpoint>` seems awkward).

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`.

~~~
dassurma
If you take a look at the issues on the repo, a lot of people agree with you.
The syntax was not final at all, we need to convince TC39 first before we can
start bikeshedding ^^

------
vincentriemer
FWIW I believe that web worker adoption is more limited by tooling/knowledge
than APIs. Web developers haven't thought about thread safety for decades (at
least for those who have been around for that long) so it's more of a mental
leap for web developers to think in terms of multiple threads.

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](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.

~~~
acjohnson55
One correction: I don't think thread safety is the issue at all. First of all,
existing Javascript may not have parallelism in user code, but it does have
concurrency. This means you absolutely have to deal with race conditions and
resource sharing. Secondly, web workers can't access the mutable state in
parallel, because they can only share data via message passing of copied data.
This is what prevents thread safety from being a concern, while still enabling
parallelism.

------
taf2
I wish SharedWorkers were not removed from Safari. They are super useful for
sharing a single websocket connection between multiple open tabs. Something
you can’t do with ServiceWorkers

~~~
streptomycin
Yeah shared worker are awesome. I love having a single instance of the core of
my app running in a worker, and then each tab the user opens is just a UI view
of that one instance. It greatly simplifies keeping things in sync across
tabs.

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 :(

~~~
echelon
What percentage of your users use Safari? If it isn't impactful to your
business, you could ask them to use Firefox or Chrome.

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.

~~~
stephenr
Pro tip: if you ask a user to install a different browser, they're just as
likely to use a different _site_ instead.

~~~
megous
Depends on the user. My GF for some reason uses FF, Edge and Chrome at the
same time, most of the time (she's not in/into tech).

So if someone asked her, she would just switched the windows.

~~~
stephenr
That sounds like a fairly rare workflow. I've heard of people using different
browsers for different things but I don't think it's common.

~~~
megous
I guess some people don't care about the browsers very much, and will just use
whatever is installed. If there are multiple browsers installed, they may
learn to use different browsers as a simple way to have different easily
distinguishable browsing contexts (different icon/app name) - instead of
having multiple windows of the same browser that looks pretty much
indistinguishable.

------
EdSharkey
I don't like how the browser has to structured clone objects sent between
threads. Seems like on low end devices, that could tie up tons of ram and have
out of memory failure modes not seen on beefier phones. Reading the article,
the author seems to advocate sending js events over from Dom thread to worker,
doing all event processing on the worker, and sending commands from the worker
back to the Dom that get processed into Dom mutations. It strikes me that OP's
approach will offload the bulk of the CPU time spent doing clones onto
workers, so at least we got that.

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...](https://developer.mozilla.org/en-
US/docs/Web/API/OffscreenCanvas)

~~~
dassurma
You could always use transferable ArrayBuffers and serialize your data with
Flatbuffers or something similar.

That being said, in all the off-main-thread apps I have written so far, the
cost of structured cloning was pretty much irrelevant.

~~~
EdSharkey
Good to know, thanks

------
simonw
Web Workers were added to Firefox ten years ago this month, so they're
definitely not new technology. I think this is a great call to action to
actually start taking advantage of them.

~~~
stefan_
People have been taking plenty advantage, they are an indispensable tool to
turn all of these CPU bugs or DRAM attacks into working browser exploits. Also
those bitcoin miners of course.

Maybe if no one else is taking advantage it is time to simply throw them out.

~~~
Matheus28
“I don’t use it, therefore no one else uses it”

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).

~~~
stefan_
I suggest you look up why they disabled it. And that is _still_ not an
application of WebWorkers!

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.

~~~
Jack000
a bit niche, but here's one of my projects - svgnest.com

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.

~~~
infinite8s
I use webworkers to run an in-memory streaming pivoting engine (compiled from
C++ to wasm) to build real-time Tableau-like dashboards that run fully in the
browser (which runs fairly well for 10M row datasets or less). Network IO and
the compute is handled in the webworker, which allows the main UI thread to
remain responsive.

------
kabwj
If you really care about the performance of your site on the mobiles that poor
people use then render server-side as much as you can.

~~~
skybrian
But they may have a bad network connection as well.

~~~
hinkley
Correct me if I'm wrong, but isn't one of the uses of web workers to do
resource management via local storage, so that multiple tabs can share one set
of resources?

Not everybody is using web workers for that, but that could address both
issues to a degree.

------
troxwalt
Not related to the article, but the design and css styling of the blog is
really interesting. Makes use of CSS variables as well as a couple properties
that I've never seen before. (--mask, font-variation-settings)

~~~
ljoshua
The --mask is probably a CSS variable.

------
jhayward
Thank goodness for Safari's reader mode. This article was unreadable without
it. Why do people do this?

~~~
hashberry
Can you elaborate? I enjoyed the layout, colors, fonts etc, but curious on why
the page is not acceptable.

------
impostervt
Really, really wish you could do canvas manipulation with web workers. There's
some experimental browser stuff, but nothing standard. I'd like to be able to
do image manipulation with web workers.

~~~
dassurma
OffscreenCanvas is exactly that! It's standardized, but only chrome has
implemented it so far, sadly.

------
chrismorgan
I’ve been experimenting with all kinds of things and ideas for making a Rust-
powered web frontend system that is designed to be _completely_ functional
with server-side rendering, with scripting on the frontend being deliberately
optional, bringing things back closer to the old-style non-isomorphic server-
side rendering, doing things like wrapping all the buttons in forms so that
they will, in the absence of local scripting to intercept it and do it on the
client side in a potentially more efficient way or with transitions or such,
leave it to the server to render the result as it would have been after that
button was clicked.

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.)

~~~
zerker2000
> 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.

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?

~~~
chrismorgan
I thought about it a bit longer after I wrote it and decided that what you
need is some sort of pure function that takes the event target DOM element
only, and decides whether the default should be prevented. For example, in
FastMail we use a function to intercept links that will essentially check
whether the href is routable (fiddlier than you might guess, unfortunately)
and not target=_blank; that could be reduced to such a pure function, though
various routes would now need to be specified in two places. Either that, or
just always pipe generated <a href> elements through some function in the
worker that annotates them—I like your idea, thanks for the thoughts!

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.

------
ebj73
I don't think it's entirely true that desktop applications use separate
threads for all tasks except UI related tasks. At least not for old win32
based applications. I think it's pretty common to do other small tasks too in
the main thread, and to spawn new threads only when doing queues or other
stuff that you know will perceptibly halt the running of the main user
interface thread.

------
ficklepickle
I really enjoyed the article! Thanks! You other posts look interesting and I
have bookmarked your blog.

A tiny typo in the second-to-last section: specture -> spectrum

Thanks again!

------
Ruphin
Hey @dassurma, can you explain your task scheduling function? As far as I can
tell it schedules a micro task, and there is no meaningful difference between
that and other microtask scheduling systems:
[https://codepen.io/ruphin/pen/qzbgYr?editors=0012](https://codepen.io/ruphin/pen/qzbgYr?editors=0012)

~~~
dassurma
All your logs are executed immediately. .then() takes a callback function!!

FYI: AsyncTask and MicroTask are equivalent, and so are my task() function and
your OnMessageTask

~~~
Ruphin
Ah, I messed up in my haste to experiment :)

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 :)

~~~
dassurma
Why do you think it's less reliable? setTimeout gets clamped by the browser to
a minimum of 4ms, so you waste a lot of time when all you want is a task
boundary.

~~~
Ruphin
I tried so many janky different ways that I don't quite remember how to
reproduce it, but I found that setTimeout was more likely to always execute
once per animationFrame, where using MessageChannel would sometimes execute
more than once per frame. I guess the minimum timeout added to setTimeout
makes it more likely to not hit twice between paints. The test setup I have
now gets pretty clean results.

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); }); }

```

------
samsonradu
One good use case I had for WebWorkers was running heavy Regex on strings.
Easy to decouple from the web app as I just had to pass the 2 strings to the
Worker (text to scan and regex expression) and get back the result array.
There is, however, a performance penalty there as the browser will ‘favor’ the
main thread to for rendering and UX.

------
pfrrp
For those interested in moving apps (or parts) into a web worker, this project
by the AMP team has some fascinating ideas (via DOM API replication in a
worker): [https://github.com/ampproject/worker-
dom](https://github.com/ampproject/worker-dom)

------
kybernetikos
Last time I used web workers (admittedly quite some time ago), they made
everything _slower_. I encourage everyone who uses them to perform tests to
ensure they're getting the benefit they think they are.

------
vijaybritto
If there was a vdom library that moved the diffing part to a web worker, would
you have used it? or is it still costly to do any diffing at all?

~~~
dassurma
It's still costly, but at least it'd not be blocking the main thread. I'd
definitely give it a try!

~~~
vijaybritto
Did you ever consider using svelte? Or was it not around when you developed
this site?

~~~
dassurma
I have played with Svelte, not used it for any project yet.

When you say, “this site”, do you mean my blog? Because that one’s fully
static using 11ty.

~~~
vijaybritto
Sorry I meant proxx.app

~~~
dassurma
Ah, no Svelte was around, but we were on a pretty tight deadline and had a
good amount of experience with Preact in the team. We didn’t feel comfortable
increasing the risk by using an unknown framework.

~~~
vijaybritto
Got it. Thanks!

------
whereareyouwow
Has anyone used web workers at the edge ... like with cloudflare workers?

~~~
floatboth
Despite the similar naming, "Web Workers" is a specific browser API that has
nothing to do with "the edge" or any server-side thing.

~~~
whereareyouwow
Point is well taken. I have not tried cloudflare workers yet. It seemed like
at least the architecture was the same. But I guess you would not have access
to the full browser API. It seems like you can still play around with request
/ response side of things though.

~~~
kentonv
Cloudflare Workers is so-named because it uses the same API as Service
Workers, which are one kind of Web Worker. That said, Cloudflare Workers
currently implements only a subset of the APIs that are normally available in
the browser, though missing APIs are being added all the time.

So, your understanding is basically correct.

(I'm the tech lead for Cloudflare Workers.)

------
zhte415
What is the use-case?

Why is static not better?

What are the edges?

~~~
dassurma
Static _is_ better for a good bunch of performance metrics. But what about
interactive content like [https://proxx.app](https://proxx.app)?

The article is mostly aimed at use-cases where some sort of logic in
JavaScript is required.

~~~
whereareyouwow
Yes @dassurma. But web workers are not everything. Your own blog post does not
score 100 :
[https://developers.google.com/speed/pagespeed/insights/?url=...](https://developers.google.com/speed/pagespeed/insights/?url=https%3A%2F%2Fdassur.ma%2Fthings%2Fwhen-
workers%2F&tab=desktop) And those issues are not necessarily addressed with
web workers. Good basic static design matters too.

~~~
dassurma
I never claimed that Web Workers are _the_ silver bullet that will absolve us
from all our problems. On the contrary, I don’t think such a silver bullet
will ever exist.

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.

~~~
whereareyouwow
I agree with you on keeping the main thread free. What are your thoughts on
WASM via workers?

~~~
dassurma
We are using loads of Wasm in Workers in
[https://squoosh.app](https://squoosh.app)

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.

------
gridlockd
I think this is BS, practically all of the performance problems on the Web are
related either to the DOM or CSS, which Web Workers can't fix.

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.

~~~
chrismorgan
Many websites have too much of _everything_ , but UI-thread JS is definitely
the most common largest performance problem that prevents you from using
pages. It’s the biggest thing where an architectural re-think has the biggest
effect—though certainly just cutting down on _all_ things will improve
performance too.

~~~
gridlockd
Can you show me an example? Like I said, I call BS on this. What website does
a non-trivial amount of pure JS on the main thread? What even is the _use-
case_? React reconciliation?

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.

~~~
chrismorgan
> What website does a non-trivial amount of pure JS on the main thread?

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.

~~~
gridlockd
> 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).

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.

