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 userScrollIntentUntil = 0
|
||||
let detachScrollIntentListeners: (() => void) | undefined
|
||||
let hasRestoredScroll = false
|
||||
let hasInitialScroll = false
|
||||
|
||||
function markUserScrollIntent() {
|
||||
const now = typeof performance !== "undefined" ? performance.now() : Date.now()
|
||||
@@ -405,7 +407,11 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
||||
|
||||
function scrollToBottomAndClamp(immediate = false) {
|
||||
scrollToBottom(immediate)
|
||||
requestAnimationFrame(() => clampScrollAfterShrink())
|
||||
if (hasInitialScroll) {
|
||||
requestAnimationFrame(() => clampScrollAfterShrink())
|
||||
} else {
|
||||
hasInitialScroll = true
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToTop(immediate = false) {
|
||||
@@ -476,9 +482,13 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
||||
|
||||
createEffect(() => {
|
||||
const target = containerRef
|
||||
const loading = props.loading
|
||||
|
||||
if (!target) return
|
||||
if (loading) return
|
||||
if (hasRestoredScroll) return
|
||||
|
||||
scrollCache.restore(target, {
|
||||
fallback: () => scrollToBottom(true),
|
||||
onApplied: (snapshot) => {
|
||||
if (snapshot) {
|
||||
setAutoScroll(snapshot.atBottom)
|
||||
@@ -490,12 +500,17 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
||||
updateScrollIndicators(target)
|
||||
},
|
||||
})
|
||||
|
||||
hasRestoredScroll = true
|
||||
})
|
||||
|
||||
let previousToken: string | undefined
|
||||
|
||||
createEffect(() => {
|
||||
const token = changeToken()
|
||||
const loading = props.loading
|
||||
|
||||
if (loading) return
|
||||
if (!token || token === previousToken) {
|
||||
return
|
||||
}
|
||||
@@ -507,6 +522,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
||||
|
||||
createEffect(() => {
|
||||
preferenceSignature()
|
||||
if (props.loading) return
|
||||
if (!autoScroll()) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,36 +46,13 @@ function ensurePartId(messageId: string, part: ClientPart, index: number): strin
|
||||
const PENDING_PART_MAX_AGE_MS = 30_000
|
||||
|
||||
function clonePart(part: ClientPart): ClientPart {
|
||||
if (!part || typeof part !== "object") {
|
||||
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
|
||||
// Cloning is intentionally disabled; message parts
|
||||
// are stored as received from the backend.
|
||||
return part
|
||||
}
|
||||
|
||||
function cloneStructuredValue<T>(value: T): T {
|
||||
if (Array.isArray(value)) {
|
||||
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
|
||||
}
|
||||
// Legacy helper kept as a no-op to avoid deep copies.
|
||||
return value
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user