perf: keep sidebar ids stable during streaming and measure marquees from the resize observer
This commit is contained in:
@@ -632,9 +632,13 @@ export const ConversationSidebar = memo(function ConversationSidebar({
|
||||
listScope,
|
||||
Boolean(conversationActionsId),
|
||||
);
|
||||
// Same ids keep the same array: conversations change on every streamed
|
||||
// update, and a new array would rerun the window's layout effect, which
|
||||
// reads scrollTop and forces a synchronous layout mid-stream.
|
||||
const filteredIdsKey = filteredConversations.map((conversation) => conversation.id).join("\n");
|
||||
const filteredIds = useMemo(
|
||||
() => filteredConversations.map((conversation) => conversation.id),
|
||||
[filteredConversations],
|
||||
() => (filteredIdsKey ? filteredIdsKey.split("\n") : []),
|
||||
[filteredIdsKey],
|
||||
);
|
||||
const listWindow = useConversationListWindow({
|
||||
ids: filteredIds,
|
||||
|
||||
@@ -54,6 +54,37 @@ describe('OverflowMarquee', () => {
|
||||
).toBe('')
|
||||
})
|
||||
|
||||
it('measures from the resize observer instead of forcing layout on mount', () => {
|
||||
let callback: ResizeObserverCallback | undefined
|
||||
class FakeResizeObserver {
|
||||
constructor(next: ResizeObserverCallback) { callback = next }
|
||||
observe(): void {}
|
||||
disconnect(): void {}
|
||||
}
|
||||
const original = globalThis.ResizeObserver
|
||||
globalThis.ResizeObserver = FakeResizeObserver as unknown as typeof ResizeObserver
|
||||
try {
|
||||
const text = '这是一个明显超过会话列表宽度的完整会话名称'
|
||||
let reads = 0
|
||||
const scrollWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'scrollWidth')
|
||||
Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { configurable: true, get: () => { reads += 1; return 280 } })
|
||||
try {
|
||||
render(<OverflowMarquee text={text} />)
|
||||
const container = screen.getByTitle(text)
|
||||
setMeasuredWidth(container, 'clientWidth', 120)
|
||||
expect(reads).toBe(0)
|
||||
callback?.([], {} as ResizeObserver)
|
||||
expect(reads).toBe(1)
|
||||
expect(container).toHaveAttribute('data-overflowing', 'true')
|
||||
} finally {
|
||||
if (scrollWidth) Object.defineProperty(HTMLElement.prototype, 'scrollWidth', scrollWidth)
|
||||
else Reflect.deleteProperty(HTMLElement.prototype, 'scrollWidth')
|
||||
}
|
||||
} finally {
|
||||
globalThis.ResizeObserver = original
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps one readable text copy while its visual track moves', () => {
|
||||
render(<OverflowMarquee text="完整会话名称" />)
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ export function OverflowMarquee({
|
||||
}, [])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
measureOverflow()
|
||||
const container = containerRef.current
|
||||
const track = trackRef.current
|
||||
if (
|
||||
@@ -62,11 +61,15 @@ export function OverflowMarquee({
|
||||
!track ||
|
||||
typeof ResizeObserver !== 'function'
|
||||
) {
|
||||
measureOverflow()
|
||||
window.addEventListener('resize', measureOverflow)
|
||||
return () =>
|
||||
window.removeEventListener('resize', measureOverflow)
|
||||
}
|
||||
|
||||
// The observer reports both elements once after layout (before paint),
|
||||
// so mounting many rows does not force a synchronous layout per row. A
|
||||
// text change re-creates the observer, whose first report re-measures.
|
||||
const observer = new ResizeObserver(measureOverflow)
|
||||
observer.observe(container)
|
||||
observer.observe(track)
|
||||
|
||||
Reference in New Issue
Block a user