lazy mount message blocks
This commit is contained in:
@@ -8,11 +8,9 @@ export function getMessageAnchorId(messageId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VIRTUAL_ITEM_MARGIN_PX = 800
|
const VIRTUAL_ITEM_MARGIN_PX = 800
|
||||||
const ESTIMATED_MESSAGE_HEIGHT = 320
|
|
||||||
const INITIAL_FORCE_MIN_ITEMS = 12
|
|
||||||
const INITIAL_FORCE_OVERSCAN = 6
|
|
||||||
|
|
||||||
interface MessageBlockListProps {
|
interface MessageBlockListProps {
|
||||||
|
|
||||||
instanceId: string
|
instanceId: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
store: () => InstanceMessageStore
|
store: () => InstanceMessageStore
|
||||||
@@ -31,50 +29,10 @@ interface MessageBlockListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MessageBlockList(props: MessageBlockListProps) {
|
export default function MessageBlockList(props: MessageBlockListProps) {
|
||||||
const [initialForceActive, setInitialForceActive] = createSignal(true)
|
|
||||||
const [initialForceInitialized, setInitialForceInitialized] = createSignal(false)
|
|
||||||
const [initialForceStartIndex, setInitialForceStartIndex] = createSignal(0)
|
|
||||||
const [, setInitialForceRemaining] = createSignal(0)
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
props.instanceId
|
|
||||||
props.sessionId
|
|
||||||
setInitialForceActive(true)
|
|
||||||
setInitialForceInitialized(false)
|
|
||||||
setInitialForceStartIndex(0)
|
|
||||||
setInitialForceRemaining(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!initialForceActive() || initialForceInitialized()) return
|
|
||||||
const ids = props.messageIds()
|
|
||||||
if (ids.length === 0) return
|
|
||||||
const viewportHeight = props.scrollContainer()?.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)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Index each={props.messageIds()}>
|
<Index each={props.messageIds()}>
|
||||||
{(messageId) => {
|
{(messageId) => {
|
||||||
const messageIndex = () => props.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 (
|
return (
|
||||||
<VirtualItem
|
<VirtualItem
|
||||||
id={getMessageAnchorId(messageId())}
|
id={getMessageAnchorId(messageId())}
|
||||||
@@ -83,8 +41,6 @@ export default function MessageBlockList(props: MessageBlockListProps) {
|
|||||||
threshold={VIRTUAL_ITEM_MARGIN_PX}
|
threshold={VIRTUAL_ITEM_MARGIN_PX}
|
||||||
placeholderClass="message-stream-placeholder"
|
placeholderClass="message-stream-placeholder"
|
||||||
virtualizationEnabled={() => !props.loading}
|
virtualizationEnabled={() => !props.loading}
|
||||||
forceVisible={forceVisible}
|
|
||||||
onMeasured={handleMeasured}
|
|
||||||
>
|
>
|
||||||
|
|
||||||
<MessageBlock
|
<MessageBlock
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { JSX, Show, Accessor, children as resolveChildren, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
import { JSX, Accessor, children as resolveChildren, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
||||||
|
|
||||||
const sizeCache = new Map<string, number>()
|
const sizeCache = new Map<string, number>()
|
||||||
const DEFAULT_MARGIN_PX = 600
|
const DEFAULT_MARGIN_PX = 600
|
||||||
@@ -133,8 +133,14 @@ export default function VirtualItem(props: VirtualItemProps) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
const virtualizationEnabled = () => (props.virtualizationEnabled ? props.virtualizationEnabled() : true)
|
const virtualizationEnabled = () => (props.virtualizationEnabled ? props.virtualizationEnabled() : true)
|
||||||
|
const shouldHideContent = createMemo(() => {
|
||||||
|
if (props.forceVisible?.()) return false
|
||||||
|
if (!virtualizationEnabled()) return false
|
||||||
|
return !isIntersecting()
|
||||||
|
})
|
||||||
|
|
||||||
|
let wrapperRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
let wrapperRef: HTMLDivElement | undefined
|
|
||||||
let contentRef: HTMLDivElement | undefined
|
let contentRef: HTMLDivElement | undefined
|
||||||
let resizeObserver: ResizeObserver | undefined
|
let resizeObserver: ResizeObserver | undefined
|
||||||
let intersectionCleanup: (() => void) | undefined
|
let intersectionCleanup: (() => void) | undefined
|
||||||
@@ -213,6 +219,7 @@ export default function VirtualItem(props: VirtualItemProps) {
|
|||||||
contentRef = element ?? undefined
|
contentRef = element ?? undefined
|
||||||
if (contentRef) {
|
if (contentRef) {
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
|
if (shouldHideContent()) return
|
||||||
updateMeasuredHeight()
|
updateMeasuredHeight()
|
||||||
setupResizeObserver()
|
setupResizeObserver()
|
||||||
})
|
})
|
||||||
@@ -220,9 +227,21 @@ export default function VirtualItem(props: VirtualItemProps) {
|
|||||||
cleanupResizeObserver()
|
cleanupResizeObserver()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (shouldHideContent()) {
|
||||||
|
cleanupResizeObserver()
|
||||||
|
} else if (contentRef) {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
updateMeasuredHeight()
|
||||||
|
setupResizeObserver()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const key = props.cacheKey
|
const key = props.cacheKey
|
||||||
|
|
||||||
const cached = sizeCache.get(key)
|
const cached = sizeCache.get(key)
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
setMeasuredHeight(cached)
|
setMeasuredHeight(cached)
|
||||||
@@ -238,13 +257,8 @@ export default function VirtualItem(props: VirtualItemProps) {
|
|||||||
refreshIntersectionObserver(root ?? null)
|
refreshIntersectionObserver(root ?? null)
|
||||||
})
|
})
|
||||||
|
|
||||||
const shouldHideContent = createMemo(() => {
|
|
||||||
if (props.forceVisible?.()) return false
|
|
||||||
if (!virtualizationEnabled()) return false
|
|
||||||
return !isIntersecting()
|
|
||||||
})
|
|
||||||
|
|
||||||
const placeholderHeight = createMemo(() => {
|
const placeholderHeight = createMemo(() => {
|
||||||
|
|
||||||
const seenHeight = measuredHeight()
|
const seenHeight = measuredHeight()
|
||||||
if (seenHeight > 0) {
|
if (seenHeight > 0) {
|
||||||
return seenHeight
|
return seenHeight
|
||||||
@@ -267,6 +281,14 @@ export default function VirtualItem(props: VirtualItemProps) {
|
|||||||
return classes.filter(Boolean).join(" ")
|
return classes.filter(Boolean).join(" ")
|
||||||
}
|
}
|
||||||
const placeholderClass = () => ["virtual-item-placeholder", props.placeholderClass].filter(Boolean).join(" ")
|
const placeholderClass = () => ["virtual-item-placeholder", props.placeholderClass].filter(Boolean).join(" ")
|
||||||
|
const lazyContent = createMemo<JSX.Element | null>(() => {
|
||||||
|
if (shouldHideContent()) return null
|
||||||
|
if (import.meta.env?.DEV) {
|
||||||
|
console.debug("rendering virtual item", props.cacheKey)
|
||||||
|
}
|
||||||
|
return resolved()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={setWrapperRef} id={props.id} class={wrapperClass()} style={{ width: "100%" }}>
|
<div ref={setWrapperRef} id={props.id} class={wrapperClass()} style={{ width: "100%" }}>
|
||||||
@@ -278,7 +300,7 @@ export default function VirtualItem(props: VirtualItemProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div ref={setContentRef} class={contentClass()}>
|
<div ref={setContentRef} class={contentClass()}>
|
||||||
{resolved()}
|
{lazyContent()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user