whatwg / html

HTML Standard
https://html.spec.whatwg.org/multipage/
Other
8k stars 2.62k forks source link

We need something like `requestFinalFrame()` to run logic after all `*Observer` callbacks #9721

Open trusktr opened 1 year ago

trusktr commented 1 year ago

The problem is clearly described in

but I'll paste it here too (click to expand): > ## But there's a problem with adding new `*Observer` patterns in general: > We have no way to derive state from multiple combinations of them for use in the browser paint cycle. > > The more `*Observer` APIs we add, the more need we have for some way to run logic after all paint-cycle observers have ran (by paint-cycle observers, I mean those that run in sync with animation frames and browser paint). > > Here's the problem, which add yet another frame-based `*Observer` will exacerbate: > > At present, if we rely on an animation frame, a `ResizeObserver`, a `MutationObserver`, and an `IntersectionObserver`, in order to react to changes of all of those, and to derive all the state we need for rendering something to a WebGL canvas, this means we will have to render to the canvas a total of 4 times in one frame, reducing our framerate to 25% (ouch!!!!!). In each callback, starting with the animation frame callback, we have to update state and then render. > > We must render in every type of callback, because we don't know, for any given render frame, which callbacks will run. > > If we render to canvas **only** in an animation frame callback (which is essentially the pattern that everyone (and I mean practically everyone) is writing today), we'll run into unintuitive issues like [this one](https://github.com/pixijs/pixijs/issues/3395) where on every resize the canvas will flicker (typically a white color on websites with default CSS background). > > Here's a demo. Set `SHOW_FLICKER_PROBLEM` to `true` to see the issue here: > > https://codepen.io/trusktr/pen/EzBKYM > > Here's what the problem looks like: > > https://github.com/whatwg/html/assets/297678/e549c44a-d893-409f-80d0-ca019d6323dc > > Then set `SOLVE_FLICKER_PROBLEM` to `true` to enable the solution, and the problem will be gone: > > https://github.com/whatwg/html/assets/297678/76226ae4-9898-4d92-820d-4c0c90698204 > > Note the comments, which will point out where the canvas is double rendered during resize, cutting the framerate of a resize in half (ouch!!). Resizing browser windows is typically sluggish already, and this will make things worse. > > This problem happens because if we resize a canvas in a `ResizeObserver` callback **after** we have already rendered in an animation frame callback (animation frame callbacks always fire before ResizeObserver callbacks) and do not render again in the resize observer callback, then the canvas pixels will be cleared. The pixel clearing is a natural consequence of resizing a canvas and has nothing to do with animation frames or resize observers, however the way these APIs work, we do not have a reliable way to handle certain state changes _after_ multiple callback groups (anim frames, observer callbacks). > > The only way to get around all issues with aimation frames and *Observer APIs with simple code is to resort to polling for all state in a single animation frame callback, but with the added performance cost of unnecessarily reading state every frame (although in some cases this can be faster than double rendering). > > To avoid polling, the solution would be very complicated, requiring use of `MutationObserver` to detect every possible change in the DOM that could possibly a client rect to change (there are many many possibly ways to change a client rect including `