Skip to content

Commit 36a91ef

Browse files
committed
Main Thread: Hardware-Sync ResizeObserver with requestAnimationFrame (#9315)
- Implemented a `Map` + `requestAnimationFrame` (rAF) throttle inside `main.addon.ResizeObserver`. - This ensures that during continuous layout thrashing (like window resizing), the Main Thread only dispatches one batched `postMessage` payload to the App Worker per physical display frame (vsync). - Added comprehensive JSDoc to explain the performance architecture and intent behind the implementation.
1 parent 215275b commit 36a91ef

1 file changed

Lines changed: 55 additions & 0 deletions

File tree

src/main/addon/ResizeObserver.mjs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,26 @@ import DomAccess from '../DomAccess.mjs';
33
import DomEvents from '../DomEvents.mjs';
44

55
/**
6+
* @summary Main thread bridge for the native DOM ResizeObserver API.
7+
*
8+
* This addon provides a centralized, highly optimized way for App Worker components to
9+
* react to DOM node size changes. Instead of components polling or setting up their own
10+
* individual observers, they register their target IDs with this singleton.
11+
*
12+
* **Performance & Throttling Architecture:**
13+
* The native `ResizeObserver` can fire multiple times per frame during continuous layout
14+
* thrashing (e.g., resizing the browser window). Sending a `postMessage` to the App Worker
15+
* for every single micro-shift would flood the worker bridge and cause severe jank.
16+
*
17+
* To prevent this, this addon acts as a hardware-synced dam:
18+
* 1. It catches all rapid-fire resize events and accumulates them in a private Map.
19+
* This ensures that if a node resizes multiple times before a paint, only the final state is kept.
20+
* 2. It uses `requestAnimationFrame` to lock the dispatch. It only flushes the Map
21+
* and sends the `postMessage` payload exactly once per physical display frame (vsync).
22+
*
23+
* This guarantees the App Worker always receives the freshest possible layout data without
24+
* ever being overwhelmed, regardless of how aggressively the DOM is mutating.
25+
*
626
* @class Neo.main.addon.ResizeObserver
727
* @extends Neo.main.addon.Base
828
*/
@@ -37,6 +57,17 @@ class NeoResizeObserver extends Base {
3757
}
3858
}
3959

60+
/**
61+
* @member {Map} #pendingEntries=new Map()
62+
* @private
63+
*/
64+
#pendingEntries = new Map()
65+
/**
66+
* @member {Number|null} #rAFId=null
67+
* @private
68+
*/
69+
#rAFId = null
70+
4071
/**
4172
* @param {Object} config
4273
*/
@@ -56,6 +87,30 @@ class NeoResizeObserver extends Base {
5687
* @protected
5788
*/
5889
onResize(entries, observer) {
90+
let me = this;
91+
92+
entries.forEach(entry => {
93+
me.#pendingEntries.set(entry.target, entry)
94+
});
95+
96+
if (!me.#rAFId) {
97+
me.#rAFId = requestAnimationFrame(() => {
98+
me.dispatchResizeEvents()
99+
})
100+
}
101+
}
102+
103+
/**
104+
* Dispatches the accumulated events and resets the queue.
105+
* @protected
106+
*/
107+
dispatchResizeEvents() {
108+
let me = this,
109+
entries = Array.from(me.#pendingEntries.values());
110+
111+
me.#rAFId = null;
112+
me.#pendingEntries.clear();
113+
59114
entries.forEach(entry => {
60115
// the content of entry is not spreadable, so we need to manually convert it
61116
// structuredClone(entry) throws a JS error => ResizeObserverEntry object could not be cloned.

0 commit comments

Comments
 (0)