mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
* perf(ui): coalesce useScrollFade observers and skip identical setState Every fade host (sidebar groups, the tab bar, dialogs) observed scroll, resize AND childList:subtree mutations, and each observer hit synchronously read layout and dispatched a setState even when the computed fade value was unchanged. In busy workspaces with frequent realtime re-renders, a route remount fired hundreds of identical dispatchSetState calls per second across the shell's fade hosts — every one going through React's lane scheduler — stretching tab switches and detail-page opens out to several seconds. A production CPU profile of the stall showed roughly half of all samples inside that dispatchSetState path. Coalesce the three sources through one requestAnimationFrame per host and only dispatch when the computed value actually changes. The first frame after mount still applies the initial mask, so the visual behavior is unchanged. Also gives packages/ui a minimal vitest setup (jsdom + @testing-library, matching packages/core and packages/views) with coverage for the mask values and a regression test asserting that a mutation/scroll storm with unchanged layout reaches React zero times. * fix(ui): keep useScrollFade scroll handling synchronous Coalescing scroll through rAF broke chat's auto-scroll handshake: its stick-to-bottom latch attributes wheel displacement to the scroll event that confirms it within the same task, and deferring the fade update (and its re-render) off that task left followOutput stale in the autoscroll suite. Scroll stays synchronous — user scrolling is too rare to need coalescing, and the storms this hook guards against come from resizes and subtree mutations, which remain rAF-coalesced. Full packages/views suite (5981 tests) passes with this split. * fix(ui): resolve useScrollFade lifecycle defects from review Two defects from the PR review: 1. The cleanup removed the wrong closure — the listener was registered as `run` but unregistered as `schedule`, so StrictMode remounts and axis changes left the old measurement attached (double reads, stale axis). Remove `run` symmetrically. 2. A synchronous scroll executed `raf = 0` while a queued frame was still alive: the frame fired anyway (resize→scroll→mutation yielded two pending frames instead of one) and survived unmount cleanup, which saw the zeroed handle. `run` now cancels the queued frame up front — a synchronous measurement absorbs it. Also refreshes the header comment that still claimed all three sources go through rAF, and adds regression tests for the three review scenarios (absorb queued frame, zero frames after unmount, listener symmetry across cleanup and axis rebuild). * chore(ui): drop unused no-var eslint directive in test setup Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Multica Eve <[email protected]> Co-authored-by: multica-agent <[email protected]>