Skip to content
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

[resize-observer] why we need ResizeObserver.takeRecords() or ResizeObserver.hasRecords() (the canvas flickering problem) #9717

Open
trusktr opened this issue Dec 17, 2023 · 1 comment

Comments

@trusktr
Copy link

trusktr commented Dec 17, 2023

Spec: https://drafts.csswg.org/resize-observer/#resize-observer-interface

There's one main problem with ResizeObserver and rendering using canvas:

  • people typically use requestAnimationFrame for animating
  • ResizeObserver is used for handling size changes
  • ResizeObserver callbacks run after animation frame callbacks
  • A user's animation frame loop typically includes an update to canvas (f.e. WebGL)
  • when ResizeObserver runs after animation frames, a second render-to-canvas will be needed so that the drawing will be correct, or else the problem of canvas flickering will happen due to resize clearing canvas pixels before browser paint. See:
  • @atotic made some suggestions that are definitely not ideal for users:
    • perform resize inside rAF, then draw

      • This is not possible, because ResizeObserver callbacks run after animation frames
      • A new animation frame can be requested so that resize happens in the next animation frame before draw, but then the drawing will always be one frame behind, making resize look clunky
    • skip drawing in rAF, draw in RO callback

      • This is not possible. The point of RO is to detect size changes, and we don't know about size changes until RO callbacks run. There's no way to know if ResizeObserver has records queued, so that we can conditionally skip drawing in rAF and instead draw in RO callbacks.

So, how do we know that we need to avoid drawing in rAF, and instead draw in an RO callback?

That's where this issue comes in.

If we had ResizeObserver.takeRecords(), we could get the records, run the resize logic in rAF, and then draw.

If we had ResizeObserver.hasRecords(), we could skip drawing in rAF, then let the draw happen in the following RO callback.


Note, if we get other observers like ComputedStyleObserver and BoundingClientRectObserver or ClientRectObserver for short, the same problem will exist with those if they run after rAF but before browser paint, and they should also have takeRecords and preferably hasRecords.


An alternative API that would help with these problems would be requestFinalFrame or requestPaintFrame.

The convention for web developers would become:

  • animate data with requestAnimationFrame
  • handle data updates in observer callbacks without worrying a drawing yet (ResizeObserver, ComputedStyleObserver, MutationObserver, etc)
  • finally use requestFinalFrame (or requestPaintFrame or whatever name we come up with) to apply final drawing/rendering.
@trusktr
Copy link
Author

trusktr commented Dec 17, 2023

A solution someone mentioned is to double buffer using two canvas (double the memory):

  • after resize (f.e. in an RO callback) immediately write the other buffer back
  • this is cheaper than re-rendering a whole 3D scene for example, but still has more overhead than ideal
  • for one frame, the visual will be stretched or shrunk without further code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants