Reduce message cloning and gate scroll work on load

This commit is contained in:
Shantur Rathore
2025-11-30 23:28:06 +00:00
parent 6d134e4dec
commit 77bfe41a8e
2 changed files with 22 additions and 29 deletions

View File

@@ -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
}

View File

@@ -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
}