Files
apes-sidebar/src/client/tab-content-memo.ts
T
Atlas Zeng 4148130aac perf(sidebar): 消除开关/拖拽的每帧 React 重渲染
- 根因:开关过渡期 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
2026-08-22 22:50:13 +08:00

47 lines
1.9 KiB
TypeScript

/**
* The memo comparator for one tab's content cell (TabContent in Sidebar.tsx).
*
* Kept as a pure module so the "what re-renders a tab cell" contract is unit
* testable: geometry/store re-renders of the Sidebar shell must NOT reconcile
* every mounted tab (xterm/CodeMirror/FileTree subtrees — issue #315), while
* locale switches (localeRevision), tab-registry updates (tabsVersion), and
* a tab MOVING BETWEEN PANES (paneId — onOpenDiff closes over the pane id,
* and moveTab reuses the SAME tab object, so only the pane id identifies the
* move) must re-render the cell.
*/
import type { SidebarTab } from './state.ts'
/** The fields whose change invalidates a tab cell's memo. Anything a cell's
* render (or its callbacks) depends on must be listed here. */
export interface TabContentMemoKey {
tab: SidebarTab
/** The pane this tab currently lives in. onOpenDiff closes over it, and
* moveTab reuses the same tab object when the tab moves panes — without
* comparing paneId the cell would keep the source pane's callback. */
paneId: string
sessionId: string
cwd: string | undefined
visible: boolean
expanded: string[]
localeRevision: string
tabsVersion: number
}
/** True when the cell may skip a re-render (all render-affecting fields
* unchanged). Callback/context identities are deliberately ignored: their
* captured dependencies are stable or covered by the compared fields
* (onReferenceFile → sessionId/cwd, onSubagentJump/onToggleDir → stable
* refs/closures, onOpenDiff → paneId). */
export function tabContentCompare(prev: TabContentMemoKey, next: TabContentMemoKey): boolean {
return (
prev.tab === next.tab &&
prev.paneId === next.paneId &&
prev.sessionId === next.sessionId &&
prev.cwd === next.cwd &&
prev.visible === next.visible &&
prev.expanded === next.expanded &&
prev.localeRevision === next.localeRevision &&
prev.tabsVersion === next.tabsVersion
)
}