Html: Proposal: self.queueMicrotask(f)

Created on 13 Jan 2016  Â·  46Comments  Â·  Source: whatwg/html

In talking with the Angular team (mostly @mhevery), there is interest in a simple low-level method for enqueuing a microtask from author code. I've heard this from a number of authors in the past. It seems simple enough and easy to add to the spec, if we can get any other implementations besides Chrome interested.

Right now authors are doing this via a number of hacks, like creating a text node and a MutationObserver and twiddling the text node every time they want to trigger a microtask, or using Promise.resolve().then(f) (which creates a promise object and stores any thrown errors there, instead of letting them propagate to "report an exception").

There are many use cases, which I'm sure we can assemble in depth, but let me just start with the following reasons which will hopefully be enough to convince people:

  • From an academic point of view, this is a low-level primitive and there's no reason to lock it up behind higher-level concepts like promises and mutation observers.
  • From a practical point of view, libraries very often want to guarantee asynchrony (to avoid Zalgo), without taking the overhead of a full "macro" task like setTimeout(f, 0).

I'd like to first focus this thread on finding implementer support: if we spec this, will you implement it? _After that_, we can start bikeshedding the name; please don't do so beforehand.

/ccing some implementers: @hober @travisleithead @bzbarsky

additioproposal

Most helpful comment

For those looking for an explanation of tasks (eg setTimeout) and microtasks https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

All 46 comments

This makes sense as a primitive to have. The only concern is what we do once it starts getting abused (see setTimeout throttling). Of course we'd have to figure that out for Promise and MutationObserver too....

I would also suggest posting to the Mozilla dev-platform mailing list to get a wider set of Mozilla people looking at this. I can do that for you if you want; please let me know.

That'd be lovely if you could; thank you.

What is the difference between window.enqueueMicrotask(f) and window.setTimeout(f) ?

setTimeout returns to the event loop and processes events.

For more information on that question I'd suggest reading the spec which has detailed sections on setTimeout and on enqueuing a microtask.

Abusing is an issue, and yes, we may need to do something to that with MutationObserver and
Promise case too. (I can see long Promise chains to be rather horrible from responsiveness point of view.) Abuse is here even more an issue than with setTimeout, since microtask is kind of an async concept, but only from js execution side, not from event loop.

We should think about abusing issues _before_ we add the API. But in general sounds like a good idea.

I think abuse is taken care of by the slow script dialog clause already: https://html.spec.whatwg.org/#killing-scripts

I'm not so sure.
We're giving a new model for running JS almost-async. That 'almost' can be hard to understand. (at least based on my experience - I've had to explain microtasks quite a few times).

Initially microtasks were for one particular case only - running MutationObserver callbacks, and in that case abusing is less likely. Promises make abusing way more likely, and raw microtask callbacks perhaps even more.

But it is not clear to me what we could do to prevent abuse.

Well, whatever you want to do, I imagine it will fall under one of

the user agent may either throw a QuotaExceededError exception, abort the script without an exception, prompt the user, or throttle script execution.

so I still think it's covered by the existing spec.

Except that if we add some limitation, it needs to work consistently across browsers.

Why? The slow script dialog doesn't work consistently across browsers today. Recursive microtasks are essentially the same as recursive functions: it allows an extra stack frame to unwind in between, but there's otherwise no difference. So I think they should be treated the same way.

ok, so you're worried about blocking UI totally with effectively while(true);
I'm also worried about moving to a model where people start to use more
window.enqueueMicrotask and less window.setTimeout..
Having several nested microtasks may easily end up pushing the next rAF callback time to be
too late. setTimeout has less that problem since it is truly async, and may just happen after rAF if needed.

I don't think enqueueMicrotask is really an alternative to setTimeout. It's specifically used when people want to do work before rendering. In other words, it's for choosing between

function f(cb) {
  if (someTest) cb();
  else {
    doAsyncThings().then(cb);
  }
}

(which is bad since it's sometimes-sync, sometimes-async) versus

function f(cb) {
  if (someTest) enqueueMicrotask(cb);
  else {
    doAsyncThings().then(cb);
  }
}

People can abuse it by not yielding to the event loop to allow rendering/RAF, for sure. But they can also do that with while(true). If you want to yield to the event loop, you don't use enqueueMicrotask.

I would like to offer my point of view as the Angular author. enqueueMicrotask, setTimeout and requestAnimationFrame fill very different need.

enqueueMicrotask is for, I need to do something outside my current frame, but before the rendering happens. If I use setTimeout I will have flicker, since the renderer will draw intermediate view.

setTimeout is for when I am doing long work, and wish to break it up into smaller turns to allow browser so breathing room.

requestAnimationFrame is well, for animation.

So these things all fill a different niche. Yes, all of them can be misused, but let's not that be enemy of useful.

Why is this not proposed as a ECMAScript feature?

ECMAScript doesn't have a good concept of the event loop.

For those looking for an explanation of tasks (eg setTimeout) and microtasks https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

@annevk - however, Promise already uses microtasks... It would be very weird to have a primitive on which the language depends, be defined separately, in a web specific context.

@mhevery still trying to understand the use cases here. If you want to do something before rendering, you could just use rAF. Why does that not work for you?

What you mean with "need to do something outside my current frame"? What is 'frame' here?

Random thought. it feels like people often want setTimeout(,0) but with guarantee it is called before the next rAF. So, true asynchronousness, but prioritized over rAF. If such scheduling type was added, the method could even throw or return false if we're about to delay rAF because of it.
Such setup would let UA to process user input between the callback calls, unlike microtask callbacks.

(I'm just trying to understand both pros and cons and also use cases for this. In general I'm in favor of adding this API, but we're too good at adding APIs to the platform and realizing later that it wasn't quite right. )

I suspect that the lack of public microtask api is why a lot of frameworks have their own next tick method, see

@smaug setTimeout and rAF can cause UI flicker (rendering partially rendered UI), and as such are not acceptable for the frameworks, as a result as @calvinmetcalf points out many frameworks have their own microtask queues. The issue is that we now have two microtask queue, one for framework and one for Promises, which mean that that the two have different priorities. Frameworks often need to schedule 100s of micro tasks, using setTimeout and rAF just does not work, since it is to slow and causes flicker.

Yes Promises is a way to schedule microtasks, but in my opinion it is backwards. The primitive is enqueueMicrotask not Promise. Promise can be implemented using enqueueMicrotask. (The opposite is also true, one can implement enqueueMicrotask using Promises but that just seems weird.) If you try to polyfil enqueueMicrotask you will see that the polyfill is way smaller then a polyfill for Promise, which is why I think that enqueueMicrotask is the lower primitive.

the whole point of rAF is to not flicker, but do async stuff as rarely as possible right before rendering is updated. Sure, if you request a new animation frame inside rAF, then that gets called later.

Hmm, 100s of microtasks. That certainly sounds like abuse of microtasks. Doing too much stuff synchronously (remember, microtask is synchronous from browser point of view).
We've tried to move to use more async APIs in the platform and now we're proposing a new synchronous API which would be possibly heavily used and has somewhat high risk of affecting
badly to responsiveness.

So, still wondering. Is there some other kind of, truly asynchronous API which frameworks could use?

@smaug---- if frameworks are already doing this today presumably they have a reason for that. And other than a microtask API there's really nothing else that'd guarantee the thing happening after the current activity, but before the next task (except for the hacks frameworks are already using).

@phistuck

however, Promise already uses microtasks... It would be very weird to have a primitive on which the language depends, be defined separately, in a web specific context.

That's not true. Promises use a host-environment agnostic concept called "jobs". When hosted by a user agent implementing the HTML Standard, jobs map to the HTML concept of microtasks. See https://html.spec.whatwg.org/#integration-with-the-javascript-job-queue.

This is clearly out of scope for ECMAScript, and the committee has (often violently) pushed back against the very word "microtasks" when it is brought up in TC39 meetings.

Hmm, 100s of microtasks. That certainly sounds like abuse of microtasks. Doing too much stuff synchronously (remember, microtask is synchronous from browser point of view).

What are you talking about? I have way over 100 lines of code that run synchronously. Sometimes I want them to run a specific order, so I use microtasks to ensure that ordering. Other times I just let them run in the source order, but you don't call that an "abuse."

I talked to @smaug---- on IRC. His issue is that user input only comes in after a tasks completes (in a new task). So adding a mechanism to queue a lot of "event-loop blocking" work is a concern. His suggestion is to have an API that ensures the callback is invoked before next-rAF. This would not block going to the next task, but would block painting.

Since you can already have a microtask API through a polyfill, perhaps we should have both.

That seems like a reasonable separate feature request in a separate GitHub issue. I am curious if any framework authors are interested in that, or just Gecko.

I don't have my "Gecko developer" hat on when discussing about specs ;)
Gecko has zero interest on that if no other browser vendors have.

Blink is interested, so I guess Gecko does not have zero interest :) Sorry, misinterpreted what "that" you are talking about. Blink is interested in hearing if framework authors are interested.

if frameworks are already doing this today presumably they have a reason for that.

@annevk Could we please ask the relevant framework authors what the use cases are? I would really like to understand whether frameworks really want explicit microtask queuing or whether they're trying to work around some browser issue (e.g. flicker with rAF, which should never happen short of bugs) or whether they're trying to accomplish something else that queueing a microtask approximates but doesn't _quite_ match.

@mhevery is in this thread. And indeed suggests rAF has issues with flickering, which seems bad. @mhevery are there tests for that? @tomdale or @wycats might be able to tell us why Ember.run is used over rAF.

Yes, I'm aware @mhevery is in this thread. And his comments about rAF flicker are why I'm all of a sudden suspicious... I would dearly love to see an example of that, because it really shouldn't happen given how rAF is supposed to work.

If frameworks call hundreds of callbacks, doing all that during rAF time might put too much pressure to the rAF and effectively rendering would get delayed.
(But still don't know why that would cause flickering)

User event comes in, framework starts processing it. In the process there
may be a lot of Promise.then sequences. These things can evaluate truly
async (as in long stretches of user wait, waiting for XHR), or they may
execute in microtask async because the data is cached. These Promise.then
chains can easily create 100s of microtasks on a reasonably sized
application. The framework wants to guarantee, that it will not render
until all microtasks are processed. (since rendering too early would be
wasted effort, as the work would be invalidated by later microtasks). If
the framework schedules rAF there is no guarantee, that browser will not
insert a render frame in between the microtasks and rAF, which would render
intermediate results (flicker to the user). To complicated the matter
further, sometimes applications want to do DOM measurements
(width/height/position), which can be only done after the framework
renders. After these measurements, the app often adjusts some positions,
which requires incremental update to the DOM.

Yes we are probably busting our frame budget, but I don't don't believe
that forcing intermediate/wrong/flicker frames is what the app developer
wants. Everyone wants 60 fps, but everyone wants the correct frames first,
before high frame rate.

On Tue, Jan 19, 2016 at 7:01 AM smaug---- [email protected] wrote:

If frameworks call hundreds of callbacks, doing all that during rAF time
might put too much pressure to the rAF and effectively rendering would get
delayed.
(But still don't know why that would cause flickering)

—
Reply to this email directly or view it on GitHub
https://github.com/whatwg/html/issues/512#issuecomment-172878861.

@mhevery Thank you for the example.

Just to make sure I understand it properly, is the framework trying to wait until the entire Promise chain completes, even if in the async case, or is it just trying to wait until all the things that can be serviced out of cached data are done before rendering? I assume the latter, right?

there is no guarantee, that browser will not insert a render frame in between the microtasks and rAF

The whole point of rAF is that there is such a guarantee: if you make some changes to the DOM or styles and in the same task or ensuing microtasks post an rAF callback, then that rAF callback will be called before the browser renders to screen any of the DOM/style changes you made.

sometimes applications want to do DOM measurements (width/height/position), which can be only done after the framework renders.

Again, just to make sure I understand correctly, the use case here is that the framework wants to render once all the cached stuff has been applied, without necessarily waiting for the truly async tasks, and the framework consumer wants to do some work after the framework renders but before the browser then renders the output of the framework. Is that a correct summary of the problem?

@bzbarsky

Just to make sure I understand it properly, is the framework trying to wait until the entire Promise chain completes, even if in the async case, or is it just trying to wait until all the things that can be serviced out of cached data are done before rendering? I assume the latter, right?

Yes, wait until everything that can be serviced is.

The whole point of rAF is that there is such a guarantee:

OK, I stand corrected, did not realized that. But if we use rAF while there is no flicker, now we are running the risk of not rendering, and showing the wrong state to an intermediate. For example let's assume that user clicks, and frameworks schedules a rAF to render. then setTimeout fires (before rAF) The issue is that setTimeout will incorrectly see a half processed state of click. Rendering is not just updating the DOM, but also propagating the data through the system, and since we did not propagate the data, setTimeout will see unfinished click work, which would create really hard to track down issues. You can think of the event and the subsequent rendering as a transaction. Nothing can be in between.

What we need is a way to schedule work on the current microtask queue. rAF is too late since there could be other VM turns in between.

Again, just to make sure I understand correctly, the use case here is that the framework wants to render once all the cached stuff has been applied, without necessarily waiting for the truly async tasks, and the framework consumer wants to do some work after the framework renders but before the browser then renders the output of the framework. Is that a correct summary of the problem?

Yes correct. Just adding, that I keep using render, but it really should be data propagation. The fact that some data propagates to the DOM and other propagates to the other part of the application is important. While DOM propagation could be delayed, (since there is no side-effect) propagating to the rest of the app can not be delayed, since we need to treat it as transaction, and fully complete it before we allow subsequent event processing.

setTimeout will see unfinished click work

Thank you, I appreciate this example.

That said, even if the framework were to append to the current microtask queue, that just pushes the problem back a level: now the setTimeout example will see the framework render completed, but a Promise callback or mutation observer might not; this is vaguely reminiscent of the "I want my app to always be on top" problem. That said, this would only apply to Promise callbacks for Promises created during the framework's handling of click, I think, and similarly only for mutation observers for mutations triggered by the framework, so maybe it's not a problem in practice...

Promise callback or mutation observer might not

But we don't guarantee that. We only guarantee that VM turns will see it consistent.

I think, and similarly only for mutation observers for mutations triggered by the framework, so maybe it's not a problem in practice...

correct, since every VM turn is started with an event (click or timer setTimeout), Promises are intermediate items, they never start a VM turn.

I think there was a similar try with setImmediate, is there any major reason to make a new spec?

setImmediate queues a task, not a microtask.

Resurrecting this old thread:

Chrome is currently implementing the proposal as specced in https://github.com/whatwg/html/pull/2789. Are any other vendors interested in shipping queueMicrotask()? Lots of discussion with Gecko folks above; what about Safari (@cdumez @hober) or Edge (@dstorey @travisleithead)?

Note how #2789 has web developer-facing text about when this is appropriate which should help at least a bit with some of the Gecko discussions here.

@dstorey, @arronei and I are glad to see the Author note. I'm a little concerned about developers easily falling into the trap of scheduling the next callback from within the callback (e.g., like rAF) that will lead to accidental infinite callbacks without a render/paint. But there's no solution I can think of to prevent that while still enabling the scenario :-). No objections here.

Thanks @travisleithead and folks! We could indeed add some examples drawing explicit equivalence between recursive queueMicrotask and recursive sync functions / sync while loops.

I'm a little unsure how we should interpret "No objections here" for the purposes of https://whatwg.org/working-mode#additions; would you support adding this to the specification?

Yes

Was this page helpful?
0 / 5 - 0 ratings