refactor: resize performance
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useId, useLayoutEffect, useRef, useState } from 'react'
|
import { useId, useLayoutEffect, useRef, useState } from 'react'
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { observeResize } from '@/lib/sharedResizeObserver'
|
||||||
|
|
||||||
export type NodeStatus = 'loading' | 'success' | 'error' | 'initial'
|
export type NodeStatus = 'loading' | 'success' | 'error' | 'initial'
|
||||||
|
|
||||||
@@ -58,27 +59,17 @@ function BorderLoadingIndicator({
|
|||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const container = containerRef.current
|
const container = containerRef.current
|
||||||
if (!container) return
|
if (!container) return
|
||||||
// Observe the first child (BaseNode) so we use its actual rendered size.
|
|
||||||
const target =
|
const target =
|
||||||
container.firstElementChild instanceof HTMLElement
|
container.firstElementChild instanceof HTMLElement
|
||||||
? container.firstElementChild
|
? container.firstElementChild
|
||||||
: container
|
: container
|
||||||
const syncMeasure = () => {
|
|
||||||
const cw = (target as HTMLElement).offsetWidth
|
const cw = (target as HTMLElement).offsetWidth
|
||||||
const ch = (target as HTMLElement).offsetHeight
|
const ch = (target as HTMLElement).offsetHeight
|
||||||
if (cw > 0 && ch > 0) {
|
if (cw > 0 && ch > 0) setMeasured({ w: cw, h: ch })
|
||||||
queueMicrotask(() => setMeasured({ w: cw, h: ch }))
|
const unObserve = observeResize(target, ({ width: w, height: h }) => {
|
||||||
}
|
if (w > 0 && h > 0) setMeasured({ w, h })
|
||||||
}
|
|
||||||
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) })
|
|
||||||
})
|
})
|
||||||
ro.observe(target)
|
return unObserve
|
||||||
return () => ro.disconnect()
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
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).
|
* 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 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.
|
* @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.
|
||||||
*/
|
*/
|
||||||
@@ -17,15 +18,12 @@ export function useResizeHeight(
|
|||||||
const el = ref.current
|
const el = ref.current
|
||||||
if (!el) return
|
if (!el) return
|
||||||
|
|
||||||
const ro = new ResizeObserver((entries) => {
|
const unobserve = observeResize(el, (size) => {
|
||||||
const entry = entries[0]
|
if (size.height > 0) setHeight(size.height)
|
||||||
if (entry?.contentRect.height != null && entry.contentRect.height > 0) {
|
|
||||||
setHeight(entry.contentRect.height)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
ro.observe(el)
|
const initial = el.getBoundingClientRect().height
|
||||||
setHeight(el.getBoundingClientRect().height)
|
if (initial > 0) setHeight(initial)
|
||||||
return () => ro.disconnect()
|
return unobserve
|
||||||
}, deps ?? [])
|
}, deps ?? [])
|
||||||
|
|
||||||
return [height, ref]
|
return [height, ref]
|
||||||
|
|||||||
40
frontend/src/lib/sharedResizeObserver.ts
Normal file
40
frontend/src/lib/sharedResizeObserver.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user