-
Notifications
You must be signed in to change notification settings - Fork 46.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Should React use requestAnimationFrame by default? #11171
Comments
No, React does not currently use rAF at all. Although in the future we plan to use it to polyfill requestIdleCallback.
Sounds good, but don’t forget not to schedule another frame if you’ve already scheduled one. |
Following on from a conversation on twitter. Rough story so far: Me:
Me:
Me:
It's true that you can do this sort of stuff yourself, but not totally easy to manage, especially across components. Some examples: If you have a component updating on an interval (like the If you're updating a component based on a If you have a component running an animation, containing a component also running an animation, the inner component's DOM gets modified twice per frame. If you have another animating component within that one, it gets modified three times per frame, and so on. Demo. I suppose I'm really excited about the rIC stuff coming to react, but I thought the above pitfalls were already solved. As in, "Update state in tasks, do your rendering in render(), React will ensure you aren't doing more work than necessary." Keen to hear more on what makes this tricky! Especially if it's something we can find a spec solution for. |
Reopening for discussion |
Today is a US holiday but I asked @acdlite to bring me up to date on his thinking about this. We actually had a rAF schedule priority earlier in the async implementation but later cut it. I don’t fully remember what happened there and will reply after I learn more about it. If I forget please ping me, there’s a lot of stuff going on and I can accidentally miss it in an old issue. |
@gaearon suggested that controlled inputs could be the issue. Eg:
Given that the browser may render between tasks, it's possible that But, unless I'm missing something, it feels like this could happen without rAF. If the input's value is changed, and a task is queued to fire I couldn't recreate the issue by flooding the task queue, which makes me think that the html spec isn't quite right. It's more likely that a single task is queued to update the input's value and fire the @RByers can you shine some light on browser behaviour here? Is it possible for the user to type in a text input, and rAF to happen (and change the input's |
I can't speak for scheduling but i a am fairly familiar with the limitations around input value setting. The main one with updates over an async boundary is that cursor position will reset if the update isn't synchronous, making typing impossible. Since react is forcibly keeping the value to the |
@jquense I'm having trouble recreating that. What am I missing? https://static-misc.glitch.me/input-test.html |
@gaearon I'm working on a totally separate non-React project where I've been trialing using I don't think I've figured it all out yet, but one part of the problem is that the callback seems to be called immediately after vsync and before any other events have occurred, which means that you will effectively be rendering with the mouse data of the previous frame (mouse events are also triggered many times within the same frame). On my computer mouse move is triggered 4 times per frame which means it should only add a delay of about 4ms, but it seems noticeably worse. I would estimate that it doubles the visual latency. This should probably apply to clicking/typing/etc too. Maybe you can pressure Edge into fixing it or the trade-off is worth it, but it doesn't live up to the promise of being the same but better it seems. EDIT: Being scheduled after vsync with events being emitted after it should obviously incur a more than 1-frame delay over just rendering out the changes immediately, but Chrome still feels faster. Maybe there's another 1-frame delay somehow caused by Edge as well that pushes it to feel more sluggish than Chrome. |
@syranide I think Safari has the same scheduling bug. I'll file one for edge too. WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=177484 |
@jakearchibald Nice! I don't think the description for the Edge-issue is entirely correct though, but I'm not sure either. Scheduling after vsync should be correct, it's just that all other events should be emitted before it, but Edge seems to emit events as they come rather than only at the beginning of a frame. Which means that your click-event is received a few milliseconds after the animation callback has been executed and before the next paint. |
@syranide https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model - rAF callbacks are in step 7.10, style calculation happens as part of 7.11, paint happens as part of 7.12. |
@jakearchibald Ah, I guess we're saying the same thing in the end. Ultimately it seems the current event model of emitting events as they come in Edge is incompatible with the spec for |
Sorry if I'm a bit out of the loop - but is there a problem doing something like (pseudo-ish code):
i.e. store the simple values on move, and do the heavy lifting only on rAF? Is there something in React itself that prohibits this solution on the app side (e.g. maybe setState will miss the window)? |
Copy-pasting @acdlite’s response to me over messenger:
|
(I think I have enough context now to answer more questions so keep them rolling) |
Mouse moves are fairly unique in that it is almost always safe to drop an event (at least if you also drop the corresponding mouse over/out). You can’t tell if one was dropped. Most events are intentional and require permanent effects such as two clicks happening the same event loop. You need the result of the first to happen before the second one. Since these events can happen multiple times in a single frame, rAF batching is not really a suitable fix to that problem. When the same effect happens with an OS driven sequence it has to be synchronous. Such as text input. Many effects can be deferred until later than next frame and that’s why are main strategy for that will be requestIdleCallback. For things that are dependent on a per frame level, such as mouse moves, animations, scrolling and touch handling, it is often important that they’re coordinated so that they happen all at once. It is also safe to drop extra events if there are any. It is also often necessary to update even if there are no other events than time. E.g. for inertial scrolling. For those we’ll have a special strategy. As mentioned before, a tricky problem is to set up the coordinators. If two parts of an app or libraries set up their own rAF it is difficult to determine which one will happen first so letting the user schedule one rAF and React schedule another to flush the work set up in one will be difficult to guarantee that they’re in the same frame. It can be [flush, setState] or [setState, flush]. It is more likely that for these a better model will not actually be to set state but to “react” to changes in global states like mouse position and time. The state is only when that cause some permanent change such as change in inertia, or application state but that doesn’t follow the same rules as per-frame rules in that they can’t be dropped. |
Hmm, not quite. I'm not talking about debouncing event handling, just the render steps. The idea is that you'd call Debouncing events might also be useful, and it's something browsers are starting to do more of, but I think it's a separate thing. Also tricky when it comes to
Something to flush the UI task queue sounds doable, but I don't really understand the problem yet, so I'm not sure if it's the right fix for the problem. Right now, it seems that React doesn't debounce rendering to rAF due to one of the following:
The stuff around input updating makes it feel like the issue could be 2 or 3, but I haven't been able to recreate the issue, or think up a mechanism that'd cause the issue. Is there a recreation of the issue anywhere? Either a reduction, or a version of React that has the problem? I'd like to poke around it and get to grips with the issue.
I don't quite follow, unless you're talking about debouncing events, which isn't what I mean. If a network response landed to update data for the current view, and a click was received to change the view, and both happened before the next render, React currently updates the DOM twice, once to update the current view (for the network request), and again to change the view (for the click). It feels like rendering should be debounced to My assumption here is that By 'rendering' I mean a component's
Unless I'm missing something, the model I'm thinking of would give you that for free. In these various events you'd update state, cheaply, perhaps 10 times a frame, but the resulting render would happen once a frame, and your state would be a result of changes made during those 10 events.
That's true. I wonder how much of an issue this would be in practice. If so, React could provide its own rAF, so it can flush after all queued callbacks. |
I've tried to create a reduction of an input that sets state on https://event-loop-tests.glitch.me/raf-debouncing-input.html |
A possible (albeit unlikely) scenario to consider might be tracing a path with the mouse. If we imagine that the entire motion took place between frames, a debounce would only have the final position rather than the whole path since setState wouldn't inherently accumulate the updates (and if it did accumulate - then it would be wasteful in most other scenarios... and if I'm not mistaken it ends up losing the optimization anyway since you'd want to process all those events before the next tick). I'm not saying this is likely or something React needs to concern itself with, but it is a possible gotcha
Another edge case to consider would be where the render target is audio, hardware, raw data - or something other than visual ticks. Both in terms of missed events and specifying a limiter other than rAF Both of those points are way outside the norm of React usage and it can't be all things for all people, but felt it's worthwhile to think these things out loud :) |
I think this is unrelated. Chrome/Firefox already debounce But even then, wouldn't you need to be maintaining an array of points so you don't lose this data on the next
If you need realtime audio processing, it really needs to be off the main thread, eg audio worklets. |
I think you meant the same thing. Andrew was being too concise, but by “default for non-input events” he means “the priority under which setStates that originate inside non-input events will be flushed”. |
I don’t think that’s what Sebastian is saying. I think what he’s saying is that it’s important for the result of every click event to flush before the next click event. Consider a button that sets This is why we think it is important to treat Andrew’s earlier point was that once we switch over to that approach, using To sum up:
Does this help at all? I might have made a mistake somewhere so I’ll ask Andrew and Seb to verify, but this is my understanding. The fifth point is not a final decision, we just haven’t gotten to it yet. We might use a strategy that’s a bit more obvious to the user or less error-prone. The intended takeaway is that: (1) I don’t see how we could use rAFs for clicks, (2) we already have a more efficient strategy in the work for non-clicks (rIC) so adding rAF in the mix doesn’t make it better, (3) we’ll need to think more about animation-specific use case, but it will likely build on top of the more flexible flushSync promitive we’ll have to expose anyway, (4) the user won’t get over-rendering animation by default as you’re worried, instead the animation will flush too rarely, leading them to research the right rAF solution (which we will document or provide a convenience API for). |
Ahhhhh yes, that makes total sense.
Yeah, given the above, this model makes sense, as does the rest of your summary.
I guess it'll also break things if anyone's already using rAF to debounce, as they'll be throttled to rIC instead. I guess this would break the clock demo too, as the timing is important. There's still the case of nested components with animations, but maybe the answer is "Don't pipe your animations through React", which seems sensible to me, but folks have tried to convince me otherwise in the past. Or, as you say, provide some specific opt-in to schedule a render debounced by rAF, but in a React-aware way, so parent & child only render once. Thank you for going through the reasoning & the future scheduling of React! |
If that happens, it sounds like a bug to me, rather than an intentional decision. I haven't seen that but if you can reproduce please file an issue and we'll look. The state changes enqueued inside event handlers should always be flushed on exiting the event handlers both in React 15 and 16. I can imagine that React 15 could have some sort of issues with multiple roots and edge cases like |
@Cryrivers that won't be affected. The event handlers aren't being deferred. |
I haven't read the whole thread, so apologies if I miss something. Replying to:
By "update event" I assume you mean "change", right? And by "rAF" to happen, you mean the rAF callback is invoked? If so, then yes I believe it is possible as follows:
/cc @dtapuska, who knows more about this than I do. |
@RByers My bad, I mean "input", or "keydown" rather than "change". I couldn't recreate the issue here https://event-loop-tests.glitch.me/raf-debouncing-input.html, but that doesn't mean it isn't possible. |
Could we use
It will output:
|
No because:
|
By default for right now - is the path from Specifically - if I have some code in |
It depends on where you call
|
In this example, you've set both the disabled attribute and checked that flag in the onClick handler. Does this mean that it's not sufficient to only set the disabled attribute and that you also need to check inside the click handler to prevent these kinds of click more than once issues? |
Thanks @gaearon and everyone - I learned a lot from following this thread! |
FWIW we've since stopped using |
^ Was curious about that and for others that are curious as well, I believe this is the polyfill code which now lives in src/packages/Scheduler.js (feel free to correct me if I'm wrong though). Thanks for calling that out :) |
Is this now possible? Coz, I remember I came across this in last few months and I've to bring up React docs to tale help of componentDidUpdate() to have the measurement of DOM nodes. Please if you can provide a link to where can I find the updated way to do something like
I try to follow major updates around in JS community but sorry if I've missed out in my heavy work schedule. |
Consider the following sample code: (pasted here too)
When outputStats is hit - I'm getting framerates of like 2000fps. In other words
requestAnimationFrame
does not seem to be a limiter for react itself.Is this correct?
(as a slightly separate topic- if that is true, for animation things do you think it would be good to simply wrap the
if (this.canRender) {}
block in arequestAnimationFrame()
? I guess that's not really a React question though since the observableThing could also be capped via ticks...)The text was updated successfully, but these errors were encountered: