- 根因:开关过渡期 ResizeObserver→setCenterRect 每帧重渲染整棵 Sidebar;TabContent 无 memo;内部 Divider/文件树 dock 每 pointermove 全量更新;拖拽残余为 `#root` 每帧 reflow(动画固有成本,A/B 实证非回归)。 - 修复:centerRect 改 ref + 底栏 DOM 直写(零 React 渲染);TabContent memo(显式比较器,locale/registry/paneId 保持新鲜);新增 frame-batcher 对 Divider/dock 拖拽按帧合并;拖拽期跳过无意义 locate。 - 验证:单测 76 文件 773 通过;`pnpm test:mount` 12/12(含新 toggle-layout e2e:开关过渡底栏逐帧 0.0px 贴合);4x CPU 节流 A/B:开关 >17ms 帧 collapse 19→6 / expand 24→4~6,p95 21ms→15ms;拖拽不变(非回归)。 - CR(@codex P1)已修复:`moveTab` 复用同一 tab 对象,比较器改显式比较 `paneId`(含回归单测)。 - 本地 DSH Web 实测通过后按用户要求合入。 fixes #315
57 lines
1.7 KiB
TypeScript
57 lines
1.7 KiB
TypeScript
/**
|
|
* Per-frame task coalescing for pointer-driven interactions.
|
|
*
|
|
* Pointer streams fire several times faster than the display refresh, and
|
|
* applying every event (a React setState, a store reduce) re-renders whole
|
|
* trees at event cadence — the visible drag lag on slower CPUs (#315). This
|
|
* batcher keeps the LATEST scheduled task, runs it at most once per
|
|
* animation frame, and lets the caller flush synchronously on release (so
|
|
* the final pointer position is never lost to a cancelled frame).
|
|
*
|
|
* Callers compose the pending value in a ref before scheduling; the
|
|
* scheduled task reads that ref, so replacing the task on every event is
|
|
* cheap (one ref write + one rAF check).
|
|
*/
|
|
export interface FrameBatcher {
|
|
/** Schedule (or replace) the per-frame task. Safe to call on every event. */
|
|
schedule(task: () => void): void
|
|
/** Cancel any pending frame and run the scheduled task synchronously
|
|
* (no-op when nothing is scheduled). */
|
|
flushNow(): void
|
|
/** Cancel any pending frame and drop the task without running it. */
|
|
dispose(): void
|
|
}
|
|
|
|
export function createFrameBatcher(): FrameBatcher {
|
|
let frame: number | null = null
|
|
let task: (() => void) | null = null
|
|
|
|
const run = (): void => {
|
|
frame = null
|
|
const current = task
|
|
task = null
|
|
current?.()
|
|
}
|
|
|
|
return {
|
|
schedule(next) {
|
|
task = next
|
|
if (frame === null) frame = requestAnimationFrame(run)
|
|
},
|
|
flushNow() {
|
|
if (frame !== null) {
|
|
cancelAnimationFrame(frame)
|
|
frame = null
|
|
}
|
|
run()
|
|
},
|
|
dispose() {
|
|
if (frame !== null) {
|
|
cancelAnimationFrame(frame)
|
|
frame = null
|
|
}
|
|
task = null
|
|
},
|
|
}
|
|
}
|