Reduce message cloning and gate scroll work on load
This commit is contained in:
@@ -332,6 +332,8 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
let pendingScrollFrame: number | null = null
|
let pendingScrollFrame: number | null = null
|
||||||
let userScrollIntentUntil = 0
|
let userScrollIntentUntil = 0
|
||||||
let detachScrollIntentListeners: (() => void) | undefined
|
let detachScrollIntentListeners: (() => void) | undefined
|
||||||
|
let hasRestoredScroll = false
|
||||||
|
let hasInitialScroll = false
|
||||||
|
|
||||||
function markUserScrollIntent() {
|
function markUserScrollIntent() {
|
||||||
const now = typeof performance !== "undefined" ? performance.now() : Date.now()
|
const now = typeof performance !== "undefined" ? performance.now() : Date.now()
|
||||||
@@ -405,7 +407,11 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
|
|
||||||
function scrollToBottomAndClamp(immediate = false) {
|
function scrollToBottomAndClamp(immediate = false) {
|
||||||
scrollToBottom(immediate)
|
scrollToBottom(immediate)
|
||||||
requestAnimationFrame(() => clampScrollAfterShrink())
|
if (hasInitialScroll) {
|
||||||
|
requestAnimationFrame(() => clampScrollAfterShrink())
|
||||||
|
} else {
|
||||||
|
hasInitialScroll = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToTop(immediate = false) {
|
function scrollToTop(immediate = false) {
|
||||||
@@ -476,9 +482,13 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const target = containerRef
|
const target = containerRef
|
||||||
|
const loading = props.loading
|
||||||
|
|
||||||
if (!target) return
|
if (!target) return
|
||||||
|
if (loading) return
|
||||||
|
if (hasRestoredScroll) return
|
||||||
|
|
||||||
scrollCache.restore(target, {
|
scrollCache.restore(target, {
|
||||||
fallback: () => scrollToBottom(true),
|
|
||||||
onApplied: (snapshot) => {
|
onApplied: (snapshot) => {
|
||||||
if (snapshot) {
|
if (snapshot) {
|
||||||
setAutoScroll(snapshot.atBottom)
|
setAutoScroll(snapshot.atBottom)
|
||||||
@@ -490,12 +500,17 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
updateScrollIndicators(target)
|
updateScrollIndicators(target)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
hasRestoredScroll = true
|
||||||
})
|
})
|
||||||
|
|
||||||
let previousToken: string | undefined
|
let previousToken: string | undefined
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const token = changeToken()
|
const token = changeToken()
|
||||||
|
const loading = props.loading
|
||||||
|
|
||||||
|
if (loading) return
|
||||||
if (!token || token === previousToken) {
|
if (!token || token === previousToken) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -507,6 +522,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
preferenceSignature()
|
preferenceSignature()
|
||||||
|
if (props.loading) return
|
||||||
if (!autoScroll()) {
|
if (!autoScroll()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,36 +46,13 @@ function ensurePartId(messageId: string, part: ClientPart, index: number): strin
|
|||||||
const PENDING_PART_MAX_AGE_MS = 30_000
|
const PENDING_PART_MAX_AGE_MS = 30_000
|
||||||
|
|
||||||
function clonePart(part: ClientPart): ClientPart {
|
function clonePart(part: ClientPart): ClientPart {
|
||||||
if (!part || typeof part !== "object") {
|
// Cloning is intentionally disabled; message parts
|
||||||
return part
|
// are stored as received from the backend.
|
||||||
}
|
return part
|
||||||
const cloned: Record<string, any> = { ...part }
|
|
||||||
if ("renderCache" in cloned) {
|
|
||||||
cloned.renderCache = undefined
|
|
||||||
}
|
|
||||||
if ("text" in cloned) {
|
|
||||||
cloned.text = cloneStructuredValue(cloned.text)
|
|
||||||
}
|
|
||||||
if ("thinking" in cloned && typeof cloned.thinking === "object") {
|
|
||||||
cloned.thinking = cloneStructuredValue(cloned.thinking)
|
|
||||||
}
|
|
||||||
if ("content" in cloned && Array.isArray(cloned.content)) {
|
|
||||||
cloned.content = cloneStructuredValue(cloned.content)
|
|
||||||
}
|
|
||||||
return cloned as ClientPart
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneStructuredValue<T>(value: T): T {
|
function cloneStructuredValue<T>(value: T): T {
|
||||||
if (Array.isArray(value)) {
|
// Legacy helper kept as a no-op to avoid deep copies.
|
||||||
return value.map((item) => cloneStructuredValue(item)) as T
|
|
||||||
}
|
|
||||||
if (value && typeof value === "object") {
|
|
||||||
const next: Record<string, any> = {}
|
|
||||||
Object.entries(value as Record<string, any>).forEach(([key, nested]) => {
|
|
||||||
next[key] = cloneStructuredValue(nested)
|
|
||||||
})
|
|
||||||
return next as T
|
|
||||||
}
|
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user