- 根因:开关过渡期 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
56 lines
2.4 KiB
TypeScript
56 lines
2.4 KiB
TypeScript
/**
|
|
* The TabContent memo comparator: geometry/store re-renders of the Sidebar
|
|
* shell must not reconcile every mounted tab (issue #315), while the fields
|
|
* a cell's output or callbacks depend on MUST invalidate it. The pane-move
|
|
* case (review P1): moveTab reuses the SAME tab object when a tab moves
|
|
* panes, so paneId — not tab identity — is what identifies the move and
|
|
* must be compared (onOpenDiff closes over the pane id).
|
|
*/
|
|
import { describe, expect, it } from 'vitest'
|
|
import { tabContentCompare, type TabContentMemoKey } from '../src/client/tab-content-memo.ts'
|
|
import type { SidebarTab } from '../src/client/state.ts'
|
|
|
|
function makeKey(overrides: Partial<TabContentMemoKey> = {}): TabContentMemoKey {
|
|
return {
|
|
tab: { id: 'tab:1', type: 'git', title: 'Git' } as SidebarTab,
|
|
paneId: 'pane:a',
|
|
sessionId: 'session:1',
|
|
cwd: '/workspace',
|
|
visible: true,
|
|
expanded: [],
|
|
localeRevision: 'en-US',
|
|
tabsVersion: 0,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
describe('tabContentCompare', () => {
|
|
it('skips re-render when nothing render-affecting changed (callback identity alone is ignored)', () => {
|
|
const key = makeKey()
|
|
expect(tabContentCompare(key, { ...key })).toBe(true)
|
|
})
|
|
|
|
it('invalidates on a tab moving between panes even though the tab OBJECT is reused (moveTab P1)', () => {
|
|
const sameTab = { id: 'tab:1', type: 'git', title: 'Git' } as SidebarTab
|
|
const before = makeKey({ tab: sameTab, paneId: 'pane:a' })
|
|
const after = makeKey({ tab: sameTab, paneId: 'pane:b' }) // moveTab reinserts the same object
|
|
expect(before.tab).toBe(after.tab)
|
|
expect(tabContentCompare(before, after)).toBe(false)
|
|
})
|
|
|
|
it('invalidates on locale switches (copy freshness)', () => {
|
|
expect(tabContentCompare(makeKey(), makeKey({ localeRevision: 'zh-CN' }))).toBe(false)
|
|
})
|
|
|
|
it('invalidates on tab-registry updates (descriptor refresh)', () => {
|
|
expect(tabContentCompare(makeKey(), makeKey({ tabsVersion: 1 }))).toBe(false)
|
|
})
|
|
|
|
it('invalidates on visible/expanded/session/cwd changes', () => {
|
|
expect(tabContentCompare(makeKey(), makeKey({ visible: false }))).toBe(false)
|
|
expect(tabContentCompare(makeKey(), makeKey({ expanded: ['/workspace'] }))).toBe(false)
|
|
expect(tabContentCompare(makeKey(), makeKey({ sessionId: 'session:2' }))).toBe(false)
|
|
expect(tabContentCompare(makeKey(), makeKey({ cwd: '/other' }))).toBe(false)
|
|
})
|
|
})
|