We would like to propose Tasklets – an API to make it easier for developers to move work off the main thread/UI thread.
Jank remains a problem. While making a lot of APIs asynchronous by default helped, some work is inherently atomic. iOS, Android and other native platforms are very strict about the usage of any APIs not critical to UI manipulation on the main thread. The web seems to be the odd one out. WebWorkers already allow developers to move work to another thread, but are rather expensive and clunky to use with only postMessage() as a means of communication.
Tasklets would allow developers to move work to another thread in a very ergonomic way. As the name suggests, a Tasklet is a Worklet that runs user code. This means it is more lightweight than a WebWorker. Also, a single OS thread could be shared between multiple tasklets, making it feasible to use on memory-restricted mobile devices as well.
Please take a look at the explainer above for usage, syntax and more details.
@iank, @dknox and myself will be happy to answer any questions you might have.
I guess the best thing I can do is refer you to the official Worklet spec from Houdini. A tasklet is a special worklet. The gist is that a single OS thread can host multiple worklets (from different origins, too!) and they have different lifetime constraints.
Since this is a promise based API, it means that the Tasklet’s role is limited to running functions on the main thread’s behalf. Have you considered how a Tasklet might initiate communication? For example, if the Tasklet was performing expensive calculations periodically, how could it send data back to the main thread?
Ah, ok, I was wondering what the EventTarget stuff was about. Ok, that makes sense then. Side note, the ability to extend EventTarget would be great to have in the main thread as well.
Going back to the perf considerations of worklets, the spec says that they have a limited global scope. Is the scope defined by each subtype of worklet, meaning will Tasklet define its global scope? If so, what things will be left out?
Yes, each subtype defines what is exposed in their scope. We haven’t figured it out in detail (there’s only a short passage in the explainer), but as a rule thumb for now: “If it’s synchronous, the API is not exposed in Tasklets”.
Other than that we’d like to make tasklets as divers and generically applicable as possible.
“If it’s synchronous, the API is not exposed in Tasklets”
Isn’t the benefit of multi-threading that you can run synchronous code without blocking? Your example has doSomeExpensiveProcessing which I presume is synchronous.
It sounds like it aims to have a similar global scope to service workers. If all these APIs are added (fetch, indexeddb, cache api etc etc) will it still be lighter than a worker?
I guess it boils down to, is this really a worklet with more APIs, or a worker with convenience? If it turns out it’s closer to a worker, the API should reflect this.
The main reason this is designed as a worklet and not a worker is so that tasklets from different origins can potentially share a single OS thread on mobile devices. To avoid one tasklet starving out others, we still want to strongly encourage mostly asynchronous work.
Yes, doSomeExpensiveProcessing() was supposed to be a block of synchronous work to show that in a tasklet it is “okay” to have these bigger chunks of synchronous work, as they don’t block rendering, but developers still need to be mindful.
If I understand your question correctly: Each call to addModule() will create a new instance of that tasklet. On which thread ends up is an implementation detail and left up the UA.
What’s important to me is that bad APIs are blacklisted rather than good APIs be whitelisted. If this API goes through I’m assuming that would essentially deprecate Web Workers, so Tasklets need to be at least as powerful. It’s already a problem that Workers are often forgotten about (ex. FormData not being supported in Safari workers). But it sounds like you’re taking the right approach here, so I’m happy.
I’m not sure that is a direct consequence. WebWorkers still have a use-case for extremely heavy synchronous work (re-encoding videos?! idk). But your concern is definitely something to keep in mind
By default (in my head at least ) here is one TaskletGlobalScope associated with the window.tasklet.
Calling tasklet.addModule('thing.js'); will load the script inside the same global by default.
The key there here is that by default libraries & user code shares the same global and thread instead of having multiple globals & threads which have higher memory cost.
Potentially tasklets on different origins could live on the same thread, but this would be up to each user-agent.
That section of the explainer is basically we don’t want sync xhrs and similar things inside the TaskletGlobalScope. Nearly every API available in a worker would be available here.
Sync APIs inside workers are ok, as a new Worker script assumes a separate thread for processing, e.g. it is fine to have a worker which does: