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,
|
listScope,
|
||||||
Boolean(conversationActionsId),
|
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(
|
const filteredIds = useMemo(
|
||||||
() => filteredConversations.map((conversation) => conversation.id),
|
() => (filteredIdsKey ? filteredIdsKey.split("\n") : []),
|
||||||
[filteredConversations],
|
[filteredIdsKey],
|
||||||
);
|
);
|
||||||
const listWindow = useConversationListWindow({
|
const listWindow = useConversationListWindow({
|
||||||
ids: filteredIds,
|
ids: filteredIds,
|
||||||
|
|||||||
@@ -54,6 +54,37 @@ describe('OverflowMarquee', () => {
|
|||||||
).toBe('')
|
).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', () => {
|
it('keeps one readable text copy while its visual track moves', () => {
|
||||||
render(<OverflowMarquee text="完整会话名称" />)
|
render(<OverflowMarquee text="完整会话名称" />)
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ export function OverflowMarquee({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
measureOverflow()
|
|
||||||
const container = containerRef.current
|
const container = containerRef.current
|
||||||
const track = trackRef.current
|
const track = trackRef.current
|
||||||
if (
|
if (
|
||||||
@@ -62,11 +61,15 @@ export function OverflowMarquee({
|
|||||||
!track ||
|
!track ||
|
||||||
typeof ResizeObserver !== 'function'
|
typeof ResizeObserver !== 'function'
|
||||||
) {
|
) {
|
||||||
|
measureOverflow()
|
||||||
window.addEventListener('resize', measureOverflow)
|
window.addEventListener('resize', measureOverflow)
|
||||||
return () =>
|
return () =>
|
||||||
window.removeEventListener('resize', measureOverflow)
|
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)
|
const observer = new ResizeObserver(measureOverflow)
|
||||||
observer.observe(container)
|
observer.observe(container)
|
||||||
observer.observe(track)
|
observer.observe(track)
|
||||||
|
|||||||
Reference in New Issue
Block a user