perf: keep sidebar ids stable during streaming and measure marquees from the resize observer

This commit is contained in:
mesalogo
2026-10-03 06:28:12 +08:00
parent 82107ad86a
commit 775763eee0
3 changed files with 41 additions and 3 deletions
+6 -2
View File
@@ -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,
+31
View File
@@ -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="完整会话名称" />)
+4 -1
View File
@@ -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)