refactor: resize performance

This commit is contained in:
2026-03-12 18:28:31 +01:00
parent a0bf9c6b70
commit e9ed508bfc
3 changed files with 64 additions and 35 deletions

View File

@@ -1,6 +1,7 @@
import { useId, useLayoutEffect, useRef, useState } from 'react'
import type { ReactNode } from 'react'
import { cn } from '@/lib/utils'
import { observeResize } from '@/lib/sharedResizeObserver'
export type NodeStatus = 'loading' | 'success' | 'error' | 'initial'
@@ -58,27 +59,17 @@ function BorderLoadingIndicator({
useLayoutEffect(() => {
const container = containerRef.current
if (!container) return
// Observe the first child (BaseNode) so we use its actual rendered size.
const target =
container.firstElementChild instanceof HTMLElement
? container.firstElementChild
: container
const syncMeasure = () => {
const cw = (target as HTMLElement).offsetWidth
const ch = (target as HTMLElement).offsetHeight
if (cw > 0 && ch > 0) {
queueMicrotask(() => setMeasured({ w: cw, h: ch }))
}
}
syncMeasure()
const ro = new ResizeObserver((entries) => {
const entry = entries[0]
if (!entry) return
const { width: cw, height: ch } = entry.contentRect
setMeasured({ w: Math.round(cw), h: Math.round(ch) })
const cw = (target as HTMLElement).offsetWidth
const ch = (target as HTMLElement).offsetHeight
if (cw > 0 && ch > 0) setMeasured({ w: cw, h: ch })
const unObserve = observeResize(target, ({ width: w, height: h }) => {
if (w > 0 && h > 0) setMeasured({ w, h })
})
ro.observe(target)
return () => ro.disconnect()
return unObserve
}, [])
useLayoutEffect(() => {

View File

@@ -1,32 +1,30 @@
import { useEffect, useRef, useState } from 'react'
import { observeResize } from '@/lib/sharedResizeObserver'
/**
* Returns the height of the observed element, updating when it resizes (e.g. node resize).
* Used to give CodeMirror and other components an explicit height that tracks their container.
* Uses a shared ResizeObserver to reduce memory (one observer for all elements).
* @param defaultHeight - Height used until the element is measured.
* @param deps - Optional dependency array; when the ref is attached to a conditionally mounted element, pass deps (e.g. [viewMode]) so the effect re-runs when the element appears.
*/
export function useResizeHeight(
defaultHeight: number,
deps?: React.DependencyList
defaultHeight: number,
deps?: React.DependencyList
): [number, React.RefObject<HTMLDivElement | null>] {
const ref = useRef<HTMLDivElement | null>(null)
const [height, setHeight] = useState(defaultHeight)
const ref = useRef<HTMLDivElement | null>(null)
const [height, setHeight] = useState(defaultHeight)
useEffect(() => {
const el = ref.current
if (!el) return
useEffect(() => {
const el = ref.current
if (!el) return
const ro = new ResizeObserver((entries) => {
const entry = entries[0]
if (entry?.contentRect.height != null && entry.contentRect.height > 0) {
setHeight(entry.contentRect.height)
}
})
ro.observe(el)
setHeight(el.getBoundingClientRect().height)
return () => ro.disconnect()
}, deps ?? [])
const unobserve = observeResize(el, (size) => {
if (size.height > 0) setHeight(size.height)
})
const initial = el.getBoundingClientRect().height
if (initial > 0) setHeight(initial)
return unobserve
}, deps ?? [])
return [height, ref]
return [height, ref]
}

View File

@@ -0,0 +1,40 @@
/**
* Single shared ResizeObserver to avoid one observer per element (memory and overhead).
* Callbacks are throttled to at most once per animation frame per element.
*/
type Size = { width: number; height: number }
const callbacks = new Map<Element, (size: Size) => void>()
const rafScheduled = new Set<Element>()
let observer: ResizeObserver | null = null
function getObserver(): ResizeObserver {
if (!observer) {
observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const cb = callbacks.get(entry.target)
if (!cb || rafScheduled.has(entry.target)) continue
rafScheduled.add(entry.target)
const { width, height } = entry.contentRect
requestAnimationFrame(() => {
rafScheduled.delete(entry.target)
callbacks.get(entry.target)?.({
width: Math.round(width),
height: Math.round(height),
})
})
}
})
}
return observer
}
export function observeResize(el: Element, callback: (size: Size) => void): () => void {
callbacks.set(el, callback)
getObserver().observe(el)
return () => {
callbacks.delete(el)
getObserver().unobserve(el)
}
}