Merge pull request #67838 from NousResearch/perf/desktop-resize-raf

perf(desktop): rAF-coalesce pane + console sash resizes
This commit is contained in:
brooklyn! 2026-07-19 22:44:14 -05:00 committed by GitHub
commit 3e23c502f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 53 additions and 6 deletions

View file

@ -7,6 +7,7 @@ import { Tip } from '@/components/ui/tooltip'
import { type Translations, useI18n } from '@/i18n'
import { isDesktopFsRemoteMode } from '@/lib/desktop-fs'
import { Bug } from '@/lib/icons'
import { rafCoalesce } from '@/lib/raf-coalesce'
import { cn } from '@/lib/utils'
import { notify, notifyError } from '@/store/notifications'
import { $previewServerRestart, failPreviewServerRestart, type PreviewTarget } from '@/store/preview'
@ -172,12 +173,16 @@ export function PreviewPane({
document.body.style.cursor = 'row-resize'
document.body.style.userSelect = 'none'
// pointermove outpaces 60fps and each setHeight reflows the webview +
// console split, so coalesce to one apply per frame (commits on cleanup).
const resize = rafCoalesce((height: number) => consoleState.setHeight(height))
const handleMove = (moveEvent: PointerEvent) => {
if (!active) {
return
}
consoleState.setHeight(clampConsoleHeight(startHeight + startY - moveEvent.clientY))
resize.push(clampConsoleHeight(startHeight + startY - moveEvent.clientY))
}
const cleanup = () => {
@ -186,6 +191,7 @@ export function PreviewPane({
}
active = false
resize.finish()
document.body.style.cursor = previousCursor
document.body.style.userSelect = previousUserSelect
handle.releasePointerCapture?.(pointerId)

View file

@ -10,6 +10,7 @@ import { useStore } from '@nanostores/react'
import { type PointerEvent as ReactPointerEvent, useCallback, useMemo, useRef, useSyncExternalStore } from 'react'
import { useContributions } from '@/contrib/react/use-contributions'
import { rafCoalesce } from '@/lib/raf-coalesce'
import { cn } from '@/lib/utils'
import { $paneStates, type PaneStateSnapshot, setPaneHeightOverride, setPaneWidthOverride } from '@/store/panes'
@ -234,9 +235,9 @@ export function TreeSplit({ node, root, rootRow }: { node: SplitNode; root?: boo
document.body.style.cursor = horizontal ? 'col-resize' : 'row-resize'
document.body.style.userSelect = 'none'
const onMove = (ev: PointerEvent) => {
const shiftPx = Math.max(lo, Math.min(hi, (horizontal ? ev.clientX : ev.clientY) - start))
// pointermove outpaces 60fps and each write relayouts the whole pane tree,
// so coalesce to one apply per frame (rafCoalesce commits on cleanup).
const applyShift = (shiftPx: number) => {
if (a.fixed) {
a.paneIds.forEach(id => setOverride(id, Math.round(a0px + shiftPx)))
}
@ -247,15 +248,21 @@ export function TreeSplit({ node, root, rootRow }: { node: SplitNode; root?: boo
if (!a.fixed && !b.fixed) {
const weights = [...node.weights]
// Convert the CLAMPED pixel sizes back to weights so the persisted
// weights always agree with what's on screen.
// Clamped px → weights so persisted weights match what's on screen.
weights[aIndex] = (a0px + shiftPx) / pxPerWeight
weights[bIndex] = (b0px - shiftPx) / pxPerWeight
setTreeSplitWeights(node.id, weights)
}
}
const resize = rafCoalesce(applyShift)
const onMove = (ev: PointerEvent) => {
resize.push(Math.max(lo, Math.min(hi, (horizontal ? ev.clientX : ev.clientY) - start)))
}
const cleanup = () => {
resize.finish()
document.body.style.cursor = restoreCursor
document.body.style.userSelect = restoreSelect

View file

@ -0,0 +1,34 @@
/** Coalesce a stream of values (pointermove positions, resize deltas) to one
* `apply` per animation frame, so a drag can't drive several layouts per frame.
* `push` records the latest value and schedules a frame; `finish` commits the
* last value and cancels any pending frame (call it on pointerup/cancel).
* `null` is the empty sentinel, so `T` must never legitimately be `null`. */
export function rafCoalesce<T>(apply: (value: T) => void): { finish: () => void; push: (value: T) => void } {
let frame: null | number = null
let pending: null | T = null
const flush = () => {
frame = null
if (pending !== null) {
apply(pending)
}
}
return {
finish() {
if (frame !== null) {
cancelAnimationFrame(frame)
frame = null
}
if (pending !== null) {
apply(pending)
}
},
push(value) {
pending = value
frame ??= requestAnimationFrame(flush)
}
}
}