question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Too many DOM updates / not enough display updates

See original GitHub issue

I’m porting a Photoshop-style cropping widget from plain JS to Preact, and ran into some serious performance problems.

During drag-and-drop, the display updates (which are triggered by setState() internally in the component) were extremely sluggish, with the rectangle clearly and visibly lagging behind the mouse pointer.

Dumbfounded, I launched a profiler and CPU monitor, and eventually started digging around in the source-code, where I found this:

export const defer = typeof Promise=='function' ? Promise.resolve().then.bind(Promise.resolve()) : setTimeout;

On a modern browser, this would resolve to a Promise, which, as far as I can figure, means that this defer function will defer for the shortest possible time - e.g. until the moment where the JS thread is idle.

For animation scenarios, this seems to mean it’s doing a lot more DOM updates than the browser (Chrome) can actually do repaints.

In other words, it’s burning CPU on calls to render() and DOM updates, leaving not enough time for the browser to keep up with smooth display updates.

I eventually found the solution in this comment:

options.debounceRendering = requestAnimationFrame

Boom! Buttery smooth display updates - at about 20-30% CPU load, whereas before I was getting choppy animation and serious lag at 90-something % 😮

Is there any practical reason why requestAnimationFrame isn’t the default?

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Reactions:2
  • Comments:13 (4 by maintainers)

github_iconTop GitHub Comments

7reactions
yaodingydcommented, Jun 8, 2018

requestAnimationFrame has too long a delay (compared to microtask)

rAF is only called at the beginning of a frame. Microtask could happen anytime during one frame, as long as no other JavaScript is mid-execution, so it could get called after a callback, or at the end of each task.

requestAnimationFrame is quite indeterministic

According to https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model, any queued requestAnimationFrame callbacks should be executed before the next style recalculation/layout/paint. Currently Chrome and Firebox follow this spec, Safari and Edge do not.

There is a lengthy thread on why React doesn’t use rAF to do render. The gist is:

  1. For interactive events (such as clicks), using rAF batching is not suitable. Because these events are usually intentional and can happen multiple times in a single frame, delaying all flushing until a rAF breaks React rendering model and developer expectations in cases where the value of event handler depends on the state, or when event handlers read the state. Thus we need to flush each of them at the end of each event.

  2. Other effects can be deferred until later than next frame is suitable for a task to handle. React uses requestIdleCallback.

  3. The above two strategy would satisfy most use cases. For animation-specific usage, provide a migration path to allow user to use rAf for rendering. So in preact we have options.debounceRendering.

6reactions
developitcommented, Jun 14, 2018

What about debouncing your setState() calls with rAF? That’s the core of the issue here - you’re changing state in response to mouse movement, but inevitably you’ll end up with a lot of useless intermediary states along the way.

Think about the options we have: everyone wants setState() to be synchronous because that defines how long it takes to get a frame out (if it were rAF-based, it wouldn’t be possible to have setState() update the current frame). At the same time, when using setState() from a mouse event there’s a use-case for state updates to be batched across entire frames.

Personally, I actually did prefer the old rAF-based default. Generally my renders produce UI, which means I have no interest in rendering faster than the browser can paint. However, for folks using a lot of async nested components, maybe sequential setStates via rAF could slow things down. Imagine a case where you have something like setState({ showDialog: true }, () => setState({ showDialog: false })) - now the render produced by showDialog=true will be painted, whereas with Promise-based render debouncing it actually gets completely dropped.

class Thing extends Component {
  onmove = e => {
    if (!this.lastMove) (requestAnimationFrame || setTimeout)(this.update);
    this.lastMove = e;
  };
  update() {
    let { clientX, clientY } = this.lastMove;
    this.lastMove = null;
    this.setState({ clientX, clientY });
  }
}

I don’t think there is one general-purpose solution here. Scheduling is an incredibly complex problem, and it’s unlikely that a generalized solution would have enough knowledge of render intent to be able to accurately predict how a given state update should be batched/applied.

I do agree that global options are problematic. We use them as a last resort, and they are really only present in Preact because they’re needed by preact-compat. Perhaps there’s potential here to replace the current options.debounceRendering global with something per-component, but that’ll make the queue implementation more complex and possibly more expensive.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Executing multiple DOM updates with JavaScript efficiently
In many cases your code won't be using these properties anyway, but if so, the usual workaround is to request all the needed...
Read more >
When DOM Updates Appear to Be Asynchronous
Despite it being synchronous, before a DOM change can be made visible on the screen, several processes occur: the render tree is updated,...
Read more >
Be proactive, not reactive - Faster DOM updates via change ...
Conclusion. At as few as 100 items, change propagation can update the DOM more than 10 times faster than VDOM diffing. While this...
Read more >
How to reduce DOM nodes in React's web application effectively
This topic is very broad for a short article, so I've decided that DOM nodes reduction will be the main discussion in this...
Read more >
DOM performance case study - Arek Nawo
Reduce DOM depth - Unnecessary complexity just makes things slower. Also, in many cases, when you update the parent node, the children may...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found