Reduce reliance on getBoundingClientRect for layout and dimensions
https://github.com/Volumetrics-io/mrjs/pull/522#issuecomment-2016995027
seems to make a significant perf hit, as mentioned by @takahirox.
I also want to suggest to report the issue to Meta because it might be a Meta platform specific.
Sorry it may be too late but let me share what I researched in Apr about that layout and/or performance issue related to bounding client rect.
First of all, the incorrect layout issue doesn't seem to be a new issue introduced by async connectedCallback, but seems to be an existing issue that had been existed even since before. Actually I locally reverted the change but I reproduced the problem.
To be more precisely, it isn't exactly an incorrect layout, but sometimes it may take a few animation frames to be reflected in the correct layout. This is a known limitation of that approach because it uses an asynchronous APIs to retrieve layout positions. Because of asynchronous, it has less impact on performance, but it can also cause a delay of a few animation frames. It's a trade-off.
However, through various experiments, I have found that there seem to be certain situations where it takes particularly long to retrieve the latest layout position. For example, when you resize a window by dragging its frame with the mouse and dragging it all the way to the edge of the screen, or clicking the maximize window button, the window is maximized. However, in those situations it seems that it may take some time to retrieve the layout position if you do not release the mouse button at this point, or need a user action (eg: click in window) to retrieve the layout position. This is not always the case, but sometimes happened. And the specific conditions are not fully understood. And unfortunately I couldn't find a way to mitigate this problem.
Solutions
Unfortunately I couldn't figure out a perfect solution. Let me list up some options you might take with this async layout update approach.
- Report the problem to browser vendors. Especially the need of a user action to fire the observer callback when maximizing a window might be a bug.
- Accept the limitation. It may be delayed but the layout position will be eventually updated.
- Enable that async approach only in immersive mode because the bad performance probably coming from reflow (caused by
getBoundingClientRect()) seems to especially significant in immersive mode. - As a forth option, I have an idea that I haven't tried yet but seems worth considering. I forget where I read it, but apparently you can reduce the performance impact by gathering all the
getBoundingClientRect()calls that might cause reflow in one place. One implementation approach would be to create a new system that callsgetBoundingClientRect()for each entity and caches the results, run this system at the beginning of each animation frame, and then other systems that need layout positions use the cached information. This would avoid layout problems since it uses a synchronous API. - To the async hack, add a process to record the time when the clientRect information was last acquired. If the time exceeds a certain threshold, execute the synchronous
getBoundingClientRect()to get the latest information.
Approaches that didn't work well
Let me also share some approaches that I tried but didn't work well.
-
Using
IntersectionObserver.takeRecords(). I thought this would avoid the problem of getting the latest layout position a few frames late because it is a synchronous API, but it seems that even with this API, it takes a few frames until the latest record will be ready, so it did not resolve the problem. -
Traversing up the DOM tree to get the layout position from the top and left of the parent elements. This can be done synchronously, but it seems to be difficult to interpret CSS strictly. Since being able to control layout with CSS is supposed to be one of the advantages of the project, so I decided not to use this approach.