Skip to content

Commit 4cdeb49

Browse files
Apply PR #26813: perf(ui): defer default-open tool bodies
2 parents 45575fe + e6e98da commit 4cdeb49

2 files changed

Lines changed: 62 additions & 17 deletions

File tree

packages/app/src/pages/session.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1836,7 +1836,7 @@ export default function Page() {
18361836
<div class="flex-1 min-h-0 overflow-hidden">
18371837
<Switch>
18381838
<Match when={params.id}>
1839-
<Show when={messagesReady()}>
1839+
<Show when={!store.deferRender && messagesReady()}>
18401840
<MessageTimeline
18411841
mobileChanges={mobileChanges()}
18421842
mobileFallback={reviewContent({

packages/ui/src/components/basic-tool.tsx

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createEffect, For, Match, on, onCleanup, Show, Switch, type JSX } from "solid-js"
1+
import { createEffect, For, Match, on, onCleanup, onMount, Show, Switch, type JSX } from "solid-js"
22
import { animate, type AnimationPlaybackControls } from "motion"
33
import { useI18n } from "../context/i18n"
44
import { createStore } from "solid-js/store"
@@ -40,26 +40,76 @@ export interface BasicToolProps {
4040
}
4141

4242
const SPRING = { type: "spring" as const, visualDuration: 0.35, bounce: 0 }
43+
const deferredMounts: Array<{ active: boolean; fn: () => void }> = []
44+
let deferredFrame: number | undefined
45+
46+
function flushDeferredMounts() {
47+
while (deferredMounts.length > 0) {
48+
// Timeline tools are mounted top-to-bottom, but the viewport starts at the latest turn.
49+
// Pop from the end so heavy default-open bodies near the bottom become interactive first.
50+
const item = deferredMounts.pop()!
51+
if (item.active) {
52+
deferredFrame = deferredMounts.length > 0 ? requestAnimationFrame(flushDeferredMounts) : undefined
53+
item.fn()
54+
return
55+
}
56+
}
57+
deferredFrame = undefined
58+
}
59+
60+
function scheduleDeferredFlush() {
61+
if (deferredFrame !== undefined) return
62+
deferredFrame = requestAnimationFrame(() => {
63+
deferredFrame = requestAnimationFrame(flushDeferredMounts)
64+
})
65+
}
66+
67+
function scheduleDeferredMount(fn: () => void) {
68+
const item = { active: true, fn }
69+
deferredMounts.push(item)
70+
scheduleDeferredFlush()
71+
return () => {
72+
item.active = false
73+
}
74+
}
75+
76+
function scheduleFrameMount(fn: () => void) {
77+
const frame = requestAnimationFrame(fn)
78+
return () => cancelAnimationFrame(frame)
79+
}
4380

4481
export function BasicTool(props: BasicToolProps) {
4582
const [state, setState] = createStore({
4683
open: props.defaultOpen ?? false,
47-
ready: props.defaultOpen ?? false,
84+
ready: !props.defer && (props.defaultOpen ?? false),
4885
})
4986
const open = () => state.open
5087
const ready = () => state.ready
5188
const pending = () => props.status === "pending" || props.status === "running"
89+
const hasChildren = () => (props.defer ? "children" in props : props.children)
5290

53-
let frame: number | undefined
91+
let cancelReady: (() => void) | undefined
5492

5593
const cancel = () => {
56-
if (frame === undefined) return
57-
cancelAnimationFrame(frame)
58-
frame = undefined
94+
cancelReady?.()
95+
cancelReady = undefined
96+
}
97+
98+
const scheduleReady = (initial = false) => {
99+
cancel()
100+
cancelReady = (initial ? scheduleDeferredMount : scheduleFrameMount)(() => {
101+
cancelReady = undefined
102+
if (!open()) return
103+
setState("ready", true)
104+
})
59105
}
60106

61107
onCleanup(cancel)
62108

109+
onMount(() => {
110+
if (props.defer && open()) scheduleReady(true)
111+
})
112+
63113
createEffect(() => {
64114
if (props.forceOpen) setState("open", true)
65115
})
@@ -75,12 +125,7 @@ export function BasicTool(props: BasicToolProps) {
75125
return
76126
}
77127

78-
cancel()
79-
frame = requestAnimationFrame(() => {
80-
frame = undefined
81-
if (!open()) return
82-
setState("ready", true)
83-
})
128+
scheduleReady()
84129
},
85130
{ defer: true },
86131
),
@@ -189,7 +234,7 @@ export function BasicTool(props: BasicToolProps) {
189234
</Switch>
190235
</div>
191236
</div>
192-
<Show when={props.children && !props.hideDetails && !props.locked && !pending()}>
237+
<Show when={hasChildren() && !props.hideDetails && !props.locked && !pending()}>
193238
<Collapsible.Arrow />
194239
</Show>
195240
</div>
@@ -219,7 +264,7 @@ export function BasicTool(props: BasicToolProps) {
219264
</Collapsible.Trigger>
220265
)}
221266
</Show>
222-
<Show when={props.animated && props.children && !props.hideDetails}>
267+
<Show when={props.animated && hasChildren() && !props.hideDetails}>
223268
<div
224269
ref={contentRef}
225270
data-slot="collapsible-content"
@@ -229,10 +274,10 @@ export function BasicTool(props: BasicToolProps) {
229274
overflow: initialOpen ? "visible" : "hidden",
230275
}}
231276
>
232-
{props.children}
277+
<Show when={!props.defer || ready()}>{props.children}</Show>
233278
</div>
234279
</Show>
235-
<Show when={!props.animated && props.children && !props.hideDetails}>
280+
<Show when={!props.animated && hasChildren() && !props.hideDetails}>
236281
<Collapsible.Content>
237282
<Show when={!props.defer || ready()}>{props.children}</Show>
238283
</Collapsible.Content>

0 commit comments

Comments
 (0)