/** * Coalesce many calls to `fn` into one per animation frame. * * Use for high-frequency layout-reading event handlers (scroll, resize, * visualViewport, ResizeObserver) where the work must happen in a frame but * doing it on every event call (60+/s for resize, hundreds/s for capture * scrolls) wastes layout/paint cycles. The returned function exposes * `.cancel()` so effect cleanup can drop a pending frame. * * Pattern: * const apply = () => { ...layout reads + setState... } * const scheduled = rafThrottle(apply) * window.addEventListener("scroll", scheduled, { passive: true, capture: true }) * return () => { * scheduled.cancel() * window.removeEventListener("scroll", scheduled, { capture: true }) * } */ export function rafThrottle( fn: (...args: TArgs) => void, ): ((...args: TArgs) => void) & { cancel: () => void } { let rafId = 0 let lastArgs: TArgs | null = null const scheduled = ((...args: TArgs) => { lastArgs = args if (rafId !== 0) return rafId = requestAnimationFrame(() => { rafId = 0 const a = lastArgs lastArgs = null if (a) fn(...a) }) }) as ((...args: TArgs) => void) & { cancel: () => void } scheduled.cancel = () => { if (rafId !== 0) { cancelAnimationFrame(rafId) rafId = 0 } lastArgs = null } return scheduled }