Precalc viewport window for virtualization

This commit is contained in:
Shantur Rathore
2025-12-02 11:49:42 +00:00
parent c614da3e3c
commit 4306147990
2 changed files with 82 additions and 27 deletions

View File

@@ -28,6 +28,9 @@ const USER_BORDER_COLOR = "var(--message-user-border)"
const ASSISTANT_BORDER_COLOR = "var(--message-assistant-border)"
const TOOL_BORDER_COLOR = "var(--message-tool-border)"
const VIRTUAL_ITEM_MARGIN_PX = 800
const ESTIMATED_MESSAGE_HEIGHT = 320
const INITIAL_FORCE_MIN_ITEMS = 12
const INITIAL_FORCE_OVERSCAN = 6
type ToolCallPart = Extract<ClientPart, { type: "tool" }>
@@ -299,11 +302,38 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
})
const [scrollElement, setScrollElement] = createSignal<HTMLDivElement | undefined>()
const [initialForceActive, setInitialForceActive] = createSignal(true)
const [initialForceInitialized, setInitialForceInitialized] = createSignal(false)
const [initialForceStartIndex, setInitialForceStartIndex] = createSignal(0)
const [initialForceRemaining, setInitialForceRemaining] = createSignal(0)
const [autoScroll, setAutoScroll] = createSignal(true)
const [showScrollTopButton, setShowScrollTopButton] = createSignal(false)
createEffect(() => {
props.instanceId
props.sessionId
setInitialForceActive(true)
setInitialForceInitialized(false)
setInitialForceStartIndex(0)
setInitialForceRemaining(0)
})
createEffect(() => {
if (!initialForceActive() || initialForceInitialized()) return
const ids = messageIds()
if (ids.length === 0) return
const viewportHeight = scrollElement()?.clientHeight ?? (typeof window !== "undefined" ? window.innerHeight : 800)
const estimatedCount = Math.min(
ids.length,
Math.max(INITIAL_FORCE_MIN_ITEMS, Math.ceil(viewportHeight / ESTIMATED_MESSAGE_HEIGHT) + INITIAL_FORCE_OVERSCAN),
)
setInitialForceStartIndex(Math.max(0, ids.length - estimatedCount))
setInitialForceRemaining(estimatedCount)
setInitialForceInitialized(true)
})
const [showScrollTopButton, setShowScrollTopButton] = createSignal(false)
const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false)
let containerRef: HTMLDivElement | undefined
let lastKnownScrollTop = 0
let lastMeasuredScrollHeight = 0
@@ -649,30 +679,46 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
</Show>
<Index each={messageIds()}>
{(messageId) => (
<VirtualItem
cacheKey={messageId()}
scrollContainer={scrollElement}
threshold={VIRTUAL_ITEM_MARGIN_PX}
placeholderClass="message-stream-placeholder"
virtualizationEnabled={() => !props.loading}
>
<MessageBlock
messageId={messageId()}
instanceId={props.instanceId}
sessionId={props.sessionId}
store={store}
messageIndexMap={messageIndexMap}
lastAssistantIndex={lastAssistantIndex}
showThinking={() => preferences().showThinkingBlocks}
thinkingDefaultExpanded={() => (preferences().thinkingBlocksExpansion ?? "expanded") === "expanded"}
showUsageMetrics={showUsagePreference}
onRevert={props.onRevert}
onFork={props.onFork}
onContentRendered={handleContentRendered}
/>
</VirtualItem>
)}
{(messageId) => {
const messageIndex = () => messageIndexMap().get(messageId()) ?? 0
const forceVisible = () => initialForceActive() && messageIndex() >= initialForceStartIndex()
const handleMeasured = () => {
if (!forceVisible()) return
setInitialForceRemaining((value) => {
const next = value > 0 ? value - 1 : 0
if (next === 0) {
setInitialForceActive(false)
}
return next
})
}
return (
<VirtualItem
cacheKey={messageId()}
scrollContainer={scrollElement}
threshold={VIRTUAL_ITEM_MARGIN_PX}
placeholderClass="message-stream-placeholder"
virtualizationEnabled={() => !props.loading}
forceVisible={forceVisible}
onMeasured={handleMeasured}
>
<MessageBlock
messageId={messageId()}
instanceId={props.instanceId}
sessionId={props.sessionId}
store={store}
messageIndexMap={messageIndexMap}
lastAssistantIndex={lastAssistantIndex}
showThinking={() => preferences().showThinkingBlocks}
thinkingDefaultExpanded={() => (preferences().thinkingBlocksExpansion ?? "expanded") === "expanded"}
showUsageMetrics={showUsagePreference}
onRevert={props.onRevert}
onFork={props.onFork}
onContentRendered={handleContentRendered}
/>
</VirtualItem>
)
}}
</Index>
</div>

View File

@@ -97,12 +97,17 @@ interface VirtualItemProps {
contentClass?: string
placeholderClass?: string
virtualizationEnabled?: Accessor<boolean>
forceVisible?: Accessor<boolean>
onMeasured?: () => void
}
export default function VirtualItem(props: VirtualItemProps) {
const resolved = resolveChildren(() => props.children)
const cachedHeight = sizeCache.get(props.cacheKey)
const [isIntersecting, setIsIntersecting] = createSignal(true)
const [measuredHeight, setMeasuredHeight] = createSignal(sizeCache.get(props.cacheKey) ?? 0)
const [measuredHeight, setMeasuredHeight] = createSignal(cachedHeight ?? 0)
const [hasMeasured, setHasMeasured] = createSignal(cachedHeight !== undefined)
let hasReportedMeasurement = Boolean(cachedHeight && cachedHeight > 0)
let pendingVisibility: boolean | null = null
let visibilityFrame: number | null = null
const flushVisibility = () => {
@@ -126,7 +131,6 @@ export default function VirtualItem(props: VirtualItemProps) {
}
})
}
const [hasMeasured, setHasMeasured] = createSignal(sizeCache.has(props.cacheKey))
const virtualizationEnabled = () => (props.virtualizationEnabled ? props.virtualizationEnabled() : true)
let wrapperRef: HTMLDivElement | undefined
@@ -156,6 +160,10 @@ export default function VirtualItem(props: VirtualItemProps) {
if (normalized > 0) {
sizeCache.set(props.cacheKey, normalized)
setHasMeasured(true)
if (!hasReportedMeasurement) {
hasReportedMeasurement = true
props.onMeasured?.()
}
}
setMeasuredHeight(normalized)
}
@@ -230,6 +238,7 @@ export default function VirtualItem(props: VirtualItemProps) {
})
const shouldHideContent = createMemo(() => {
if (props.forceVisible?.()) return false
if (!virtualizationEnabled()) return false
if (!hasMeasured()) return false
return !isIntersecting()