perf(sidebar): 避免已定位会话列时重复查询 DOM
- 缓存会话列仍连接时跳过 #root 全树 querySelector - 保留 detached/HMR 回退定位与 1.5s safety net - 补充 locator 行为测试覆盖缓存复用与节点替换
This commit is contained in:
committed by
Menghuan1918
parent
2088ef6891
commit
a6c7cf63c1
@@ -47,6 +47,7 @@ import { IconPanelBottomOutline16, IconPanelRightOutline16 } from './icons.tsx'
|
||||
import { Workbench, type WorkbenchActions } from './split-pane.tsx'
|
||||
import { isNarrowWidth, useViewportSize } from './breakpoints.ts'
|
||||
import { layoutPushSize } from './layout-push.ts'
|
||||
import { resolveCenterColumn } from './center-column.ts'
|
||||
import { parseDesktopEnv } from './desktop-env.ts'
|
||||
import { getWcoSnapshot, subscribeWco } from './wco.ts'
|
||||
import { getShellPreset } from './shell-presets.ts'
|
||||
@@ -738,8 +739,10 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
// (observed: a 1px sliver at the viewport's left edge).
|
||||
const locate = (): void => {
|
||||
if (disposed) return
|
||||
const col = document.querySelector('#root [data-slot="conversation"]')
|
||||
?.parentElement as HTMLElement | undefined
|
||||
// Hot path (#403): streaming output mutates #root at token cadence.
|
||||
// Reuse a still-connected center column and only query after boot/HMR
|
||||
// detached the cached node.
|
||||
const col = resolveCenterColumn(centerColRef.current)
|
||||
if (col === undefined || !col.isConnected) {
|
||||
if (centerColRef.current !== null) {
|
||||
centerColRef.current = null
|
||||
@@ -771,7 +774,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
// Mid-drag every frame writes --dsh-sidebar-* on <html>'s style
|
||||
// attribute, which is the mutation this watcher observes — relocating
|
||||
// per drag frame is pointless (the center column node cannot change
|
||||
// while the pointer is captured) and adds a querySelector to every
|
||||
// while the pointer is captured) and adds locator work to every
|
||||
// frame's budget (#315). The 1.5s retry below still covers any node
|
||||
// swap that somehow lands mid-drag.
|
||||
if (draggingRef.current) return
|
||||
@@ -798,8 +801,9 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
// may end up byte-identical, and the col may be swapped before the
|
||||
// subtree watcher attaches). A slow unconditional re-locate makes the
|
||||
// panel converge on the real column within a couple of seconds no
|
||||
// matter what sequence the shell used. locate() is cheap when nothing
|
||||
// changed (one querySelector + an identity compare; no forced layout).
|
||||
// matter what sequence the shell used. locate() is query-free while the
|
||||
// cached column stays connected; only a detached/missing cache falls
|
||||
// back to the document selector.
|
||||
const retry = window.setInterval(locate, 1500)
|
||||
return () => {
|
||||
disposed = true
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/** Stable selector for the DSH AppFrame conversation slot. */
|
||||
const CENTER_COLUMN_SELECTOR = '#root [data-slot="conversation"]'
|
||||
|
||||
/**
|
||||
* Resolve the AppFrame center column while keeping the hot path query-free.
|
||||
*
|
||||
* Streaming chat output mutates `#root` at token cadence, so Sidebar's
|
||||
* MutationObserver can call this once per animation frame. Once the center
|
||||
* column has been found, its DOM identity is stable for the conversation
|
||||
* lifecycle; checking `isConnected` is enough to reuse it without scanning
|
||||
* the whole app tree again. Boot/HMR replacement still falls through to the
|
||||
* document query as soon as the cached node is detached.
|
||||
*/
|
||||
export function resolveCenterColumn(
|
||||
current: HTMLElement | null,
|
||||
query: () => Element | null = () => document.querySelector(CENTER_COLUMN_SELECTOR),
|
||||
): HTMLElement | undefined {
|
||||
if (current !== null && current.isConnected) return current
|
||||
|
||||
const col = query()?.parentElement
|
||||
return col !== null && col !== undefined && col.isConnected ? col : undefined
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { resolveCenterColumn } from '../src/client/center-column.ts'
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
function connectedColumn(): { col: HTMLDivElement; slot: HTMLDivElement } {
|
||||
const col = document.createElement('div')
|
||||
const slot = document.createElement('div')
|
||||
slot.dataset.slot = 'conversation'
|
||||
col.append(slot)
|
||||
document.body.append(col)
|
||||
return { col, slot }
|
||||
}
|
||||
|
||||
describe('center-column locator (issue #403)', () => {
|
||||
it('reuses a connected cached column without querying the app tree', () => {
|
||||
const { col } = connectedColumn()
|
||||
const query = vi.fn<() => Element | null>(() => null)
|
||||
|
||||
expect(resolveCenterColumn(col, query)).toBe(col)
|
||||
expect(query).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('queries and adopts the replacement after the cached column detaches', () => {
|
||||
const stale = connectedColumn().col
|
||||
stale.remove()
|
||||
const { col: replacement, slot } = connectedColumn()
|
||||
const query = vi.fn<() => Element | null>(() => slot)
|
||||
|
||||
expect(resolveCenterColumn(stale, query)).toBe(replacement)
|
||||
expect(query).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('queries when no column has been cached yet', () => {
|
||||
const { col, slot } = connectedColumn()
|
||||
const query = vi.fn<() => Element | null>(() => slot)
|
||||
|
||||
expect(resolveCenterColumn(null, query)).toBe(col)
|
||||
expect(query).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('rejects a missing or detached query result', () => {
|
||||
expect(resolveCenterColumn(null, () => null)).toBeUndefined()
|
||||
|
||||
const detachedCol = document.createElement('div')
|
||||
const detachedSlot = document.createElement('div')
|
||||
detachedCol.append(detachedSlot)
|
||||
expect(resolveCenterColumn(null, () => detachedSlot)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user