perf(ui): drop virtualized DOM in hidden panes

Add DOM instrumentation tags and harden VirtualItem visibility for hidden/zero-sized roots to prevent inactive instances from keeping heavy tool-call markup mounted; restore message stream virtualization margin.
This commit is contained in:
Shantur Rathore
2026-02-28 14:13:42 +00:00
parent c51e71c7a2
commit ca2b3c232f
10 changed files with 161 additions and 25 deletions

View File

@@ -496,17 +496,24 @@ const App: Component = () => {
const isActiveInstance = () => activeInstanceId() === instance.id
const isVisible = () => isActiveInstance() && !showFolderSelection()
return (
<div class="flex-1 min-h-0 overflow-hidden" style={{ display: isVisible() ? "flex" : "none" }}>
<InstanceMetadataProvider instance={instance}>
<InstanceShell
instance={instance}
escapeInDebounce={escapeInDebounce()}
paletteCommands={paletteCommands}
onCloseSession={(sessionId) => handleCloseSession(instance.id, sessionId)}
onNewSession={() => handleNewSession(instance.id)}
handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)}
handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)}
onExecuteCommand={executeCommand}
<div
class="flex-1 min-h-0 overflow-hidden"
style={{ display: isVisible() ? "flex" : "none" }}
data-instance-id={instance.id}
data-instance-active={isActiveInstance() ? "true" : "false"}
data-instance-visible={isVisible() ? "true" : "false"}
>
<InstanceMetadataProvider instance={instance}>
<InstanceShell
instance={instance}
isActiveInstance={isActiveInstance()}
escapeInDebounce={escapeInDebounce()}
paletteCommands={paletteCommands}
onCloseSession={(sessionId) => handleCloseSession(instance.id, sessionId)}
onNewSession={() => handleNewSession(instance.id)}
handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)}
handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)}
onExecuteCommand={executeCommand}
tabBarOffset={isPhoneLayout() && mobileFullscreenMode() ? 0 : instanceTabBarHeight()}
mobileFullscreenMode={isPhoneLayout() && mobileFullscreenMode()}
onEnterMobileFullscreen={() => void enterMobileFullscreen()}

View File

@@ -62,6 +62,9 @@ const log = getLogger("session")
interface InstanceShellProps {
instance: Instance
// Provided by App-level instance tabs; lets us pause heavy rendering
// work for inactive instances while keeping them mounted for fast switching.
isActiveInstance?: boolean
escapeInDebounce: boolean
paletteCommands: Accessor<Command[]>
onCloseSession: (sessionId: string) => Promise<void> | void
@@ -800,12 +803,14 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
>
<For each={cachedSessionIds()}>
{(sessionId) => {
const isActive = () => activeSessionIdForInstance() === sessionId
const isActive = () => Boolean(props.isActiveInstance) && activeSessionIdForInstance() === sessionId
return (
<div
class="session-cache-pane flex flex-col flex-1 min-h-0"
style={{ display: isActive() ? "flex" : "none" }}
data-session-id={sessionId}
data-instance-id={props.instance.id}
data-session-active={isActive() ? "true" : "false"}
aria-hidden={!isActive()}
>
<SessionView
@@ -841,7 +846,10 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
return (
<>
<div class="instance-shell2 flex flex-col flex-1 min-h-0">
<div
class="instance-shell2 flex flex-col flex-1 min-h-0"
data-instance-id={props.instance.id}
>
<Show when={hasSessions()} fallback={<InstanceWelcomeView instance={props.instance} />}>
{sessionLayout}
</Show>

View File

@@ -202,5 +202,15 @@ export function Markdown(props: MarkdownProps) {
const proseClass = () => "markdown-body"
return <div ref={containerRef} class={proseClass()} innerHTML={html()} />
return (
<div
ref={containerRef}
class={proseClass()}
data-view="markdown"
data-part-id={resolved().partId}
data-markdown-theme={resolved().themeKey}
data-markdown-highlight={resolved().highlightEnabled ? "true" : "false"}
innerHTML={html()}
/>
)
}

View File

@@ -8,7 +8,7 @@ export function getMessageAnchorId(messageId: string) {
return `message-anchor-${messageId}`
}
const VIRTUAL_ITEM_MARGIN_PX = 300
const VIRTUAL_ITEM_MARGIN_PX = 800
interface MessageBlockListProps {
instanceId: string

View File

@@ -381,7 +381,15 @@ export default function MessageItem(props: MessageItemProps) {
return (
<div class={containerClass()}>
<div
class={containerClass()}
data-view="message-item"
data-instance-id={props.instanceId}
data-session-id={props.sessionId}
data-message-id={props.record.id}
data-message-role={isUser() ? "user" : "assistant"}
data-message-status={props.record.status}
>
<header class={`message-item-header ${isUser() ? "pb-0.5" : "pb-0"}`}>
<div class="message-item-header-row message-item-header-row--top" ref={(el) => (topRowEl = el)}>
<div class="message-header-left">

View File

@@ -131,7 +131,12 @@ export default function MessagePart(props: MessagePartProps) {
<Switch>
<Match when={partType() === "text"}>
<Show when={!shouldHideTextPart() && partHasRenderableText(props.part)}>
<div class={canRenderMarkdown() ? markdownContainerClass() : textContainerClass()} data-role={textContainerRole()}>
<div
class={canRenderMarkdown() ? markdownContainerClass() : textContainerClass()}
data-role={textContainerRole()}
data-part-type="text"
data-part-id={typeof (props.part as any)?.id === "string" ? (props.part as any).id : undefined}
>
<Show when={canRenderMarkdown()} fallback={<span class="text-primary">{plainTextContent()}</span>}>
<Markdown
part={createTextPartForMarkdown()}

View File

@@ -913,13 +913,30 @@ export default function MessageSection(props: MessageSectionProps) {
})
return (
<div class="message-stream-container">
<div
class="message-stream-container"
data-instance-id={props.instanceId}
data-session-id={props.sessionId}
data-stream-active={isActive() ? "true" : "false"}
>
<div
class={`message-layout${hasTimelineSegments() ? " message-layout--with-timeline" : ""}`}
data-scroll-buttons={scrollButtonsCount()}
>
<div class="message-stream-shell" ref={setShellElement}>
<div class="message-stream" ref={setContainerRef} onScroll={handleScroll} onMouseUp={handleStreamMouseUp}>
<div
class="message-stream-shell"
ref={setShellElement}
data-instance-id={props.instanceId}
data-session-id={props.sessionId}
>
<div
class="message-stream"
ref={setContainerRef}
onScroll={handleScroll}
onMouseUp={handleStreamMouseUp}
data-instance-id={props.instanceId}
data-session-id={props.sessionId}
>
<div ref={setTopSentinel} aria-hidden="true" style={{ height: "1px" }} />
<Show when={!props.loading && messageIds().length === 0}>
<div class="empty-state">

View File

@@ -411,7 +411,14 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
})
return (
<div class="message-timeline" role="navigation" aria-label={t("messageTimeline.ariaLabel")}>
<div
class="message-timeline"
role="navigation"
aria-label={t("messageTimeline.ariaLabel")}
data-view="timeline"
data-instance-id={props.instanceId}
data-session-id={props.sessionId}
>
<For each={props.segments}>
{(segment) => {
onCleanup(() => buttonRefs.delete(segment.id))

View File

@@ -976,6 +976,12 @@ export default function ToolCall(props: ToolCallProps) {
setToolCallRootEl(element || undefined)
}}
class={`tool-call ${combinedStatusClass()}`}
data-part-type="tool"
data-tool-name={toolName()}
data-instance-id={props.instanceId}
data-session-id={props.sessionId}
data-message-id={props.messageId}
data-part-id={toolCallIdentifier()}
>
<div class="tool-call-header">
<button

View File

@@ -62,6 +62,42 @@ function shouldRenderEntry(entry: IntersectionObserverEntry) {
return true
}
function getViewportRect(): { top: number; bottom: number } {
if (typeof window === "undefined") {
return { top: 0, bottom: 0 }
}
return { top: 0, bottom: window.innerHeight }
}
function isRenderableRoot(root: ObserverRoot): boolean {
if (!root) return true
if (root instanceof Document) return true
if (typeof window === "undefined") return false
const element = root as Element
const style = window.getComputedStyle(element as Element)
if (style.display === "none" || style.visibility === "hidden") {
return false
}
const rect = (element as Element).getBoundingClientRect()
return rect.width > 0 && rect.height > 0
}
function shouldRenderByRects(params: {
wrapperRect: DOMRect
rootRect: { top: number; bottom: number }
margin: number
}): boolean {
const { wrapperRect, rootRect, margin } = params
const distanceAbove = rootRect.top - wrapperRect.bottom
const distanceBelow = wrapperRect.top - rootRect.bottom
const threshold = margin + VISIBILITY_BUFFER_PX
if (distanceAbove > threshold || distanceBelow > threshold) {
return false
}
return true
}
function subscribeToSharedObserver(
target: Element,
root: ObserverRoot,
@@ -120,7 +156,10 @@ interface VirtualItemProps {
export default function VirtualItem(props: VirtualItemProps) {
const resolved = resolveChildren(() => props.children)
const cachedHeight = sizeCache.get(props.cacheKey)
const [isIntersecting, setIsIntersecting] = createSignal(true)
// Default to hidden until we can determine visibility.
// This avoids keeping heavy DOM alive when IntersectionObserver
// doesn't fire (common for hidden/zero-sized scroll roots).
const [isIntersecting, setIsIntersecting] = createSignal(false)
const [measuredHeight, setMeasuredHeight] = createSignal(cachedHeight ?? 0)
const [hasMeasured, setHasMeasured] = createSignal(cachedHeight !== undefined)
let hasReportedMeasurement = Boolean(cachedHeight && cachedHeight > 0)
@@ -148,12 +187,12 @@ export default function VirtualItem(props: VirtualItemProps) {
})
}
const virtualizationEnabled = () => (props.virtualizationEnabled ? props.virtualizationEnabled() : true)
const measurementsSuspended = () => Boolean(props.suspendMeasurements?.())
const shouldHideContent = createMemo(() => {
if (props.forceVisible?.()) return false
if (!virtualizationEnabled()) return false
return !isIntersecting()
})
const measurementsSuspended = () => Boolean(props.suspendMeasurements?.())
let wrapperRef: HTMLDivElement | undefined
@@ -229,14 +268,44 @@ export default function VirtualItem(props: VirtualItemProps) {
function refreshIntersectionObserver(targetRoot: Element | Document | null) {
cleanupIntersectionObserver()
if (!wrapperRef) {
setIsIntersecting(true)
setIsIntersecting(false)
return
}
if (typeof IntersectionObserver === "undefined") {
setIsIntersecting(true)
return
}
const margin = props.threshold ?? DEFAULT_MARGIN_PX
// If the scroll root is hidden / 0x0, IntersectionObserver can report
// `isIntersecting` in unexpected ways (often "true" with null rootBounds),
// which keeps heavy DOM alive in background tabs.
//
// In that state, force-hide and skip attaching the observer. When the
// pane becomes visible again, VirtualItem will re-run this setup and
// re-attach the observer.
const renderable = isRenderableRoot(targetRoot)
if (!renderable) {
setIsIntersecting(false)
return
}
// Compute an immediate best-effort visibility so switching tabs doesn't
// depend on the first IntersectionObserver callback.
try {
const rootRect =
targetRoot && !(targetRoot instanceof Document)
? (targetRoot as Element).getBoundingClientRect()
: null
const bounds = rootRect ? { top: rootRect.top, bottom: rootRect.bottom } : getViewportRect()
setIsIntersecting(
shouldRenderByRects({ wrapperRect: wrapperRef.getBoundingClientRect(), rootRect: bounds, margin }),
)
} catch {
// Ignore measurement failures; IntersectionObserver will correct us.
}
intersectionCleanup = subscribeToSharedObserver(wrapperRef, targetRoot, margin, (entry) => {
const nextVisible = shouldRenderEntry(entry)
queueVisibility(nextVisible)
@@ -340,4 +409,3 @@ export default function VirtualItem(props: VirtualItemProps) {
</div>
)
}