Browse Source
On 144Hz+ displays, requestAnimationFrame fires ~144 times/sec. This adds a throttledRAF utility that limits callback execution to ~60fps regardless of display refresh rate, normalizing performance across 60Hz, 120Hz, and 144Hz monitors. Applied to: - workflowStore hover debounce (event-driven throttle) - FloatingNodeHeader position tracking (continuous loop throttle) - ModelParameters ResizeObserver (event-driven throttle) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>handoff-20260429-1057
4 changed files with 88 additions and 17 deletions
@ -0,0 +1,75 @@ |
|||
const FRAME_BUDGET = 1000 / 60; // ~16.67ms → 60fps cap
|
|||
|
|||
/** |
|||
* Creates a throttled requestAnimationFrame scheduler that limits |
|||
* callback execution to ~60fps regardless of display refresh rate. |
|||
* |
|||
* On 144Hz displays, RAF fires every ~6.9ms. This utility coalesces |
|||
* rapid schedule() calls and skips execution when less than one 60fps |
|||
* frame budget has elapsed, so only the latest callback runs at most |
|||
* once per ~16ms. |
|||
* |
|||
* For event-driven use (hover, ResizeObserver): the event source |
|||
* keeps firing, so skipped frames naturally get picked up by the |
|||
* next event. |
|||
*/ |
|||
export function createThrottledRAF() { |
|||
let rafId: number | null = null; |
|||
let lastTime: number | null = null; |
|||
|
|||
function schedule(callback: () => void): void { |
|||
if (rafId !== null) cancelAnimationFrame(rafId); |
|||
rafId = requestAnimationFrame((now) => { |
|||
rafId = null; |
|||
if (lastTime !== null && now - lastTime < FRAME_BUDGET) return; |
|||
lastTime = now; |
|||
callback(); |
|||
}); |
|||
} |
|||
|
|||
function cancel(): void { |
|||
if (rafId !== null) { |
|||
cancelAnimationFrame(rafId); |
|||
rafId = null; |
|||
} |
|||
} |
|||
|
|||
return { schedule, cancel }; |
|||
} |
|||
|
|||
/** |
|||
* Creates a continuous animation loop throttled to ~60fps. |
|||
* |
|||
* Unlike createThrottledRAF (which is event-driven and relies on |
|||
* external events to re-trigger), this runs a self-scheduling loop |
|||
* that executes the callback at most once per ~16ms frame budget. |
|||
* The RAF loop itself runs at the display's native rate but skips |
|||
* work on intermediate frames. |
|||
*/ |
|||
export function createThrottledLoop(callback: () => void) { |
|||
let rafId: number | null = null; |
|||
let lastTime = 0; |
|||
|
|||
function tick(now: number) { |
|||
if (now - lastTime >= FRAME_BUDGET) { |
|||
lastTime = now; |
|||
callback(); |
|||
} |
|||
rafId = requestAnimationFrame(tick); |
|||
} |
|||
|
|||
function start(): void { |
|||
if (rafId !== null) return; |
|||
lastTime = 0; // Execute immediately on first frame
|
|||
rafId = requestAnimationFrame(tick); |
|||
} |
|||
|
|||
function stop(): void { |
|||
if (rafId !== null) { |
|||
cancelAnimationFrame(rafId); |
|||
rafId = null; |
|||
} |
|||
} |
|||
|
|||
return { start, stop }; |
|||
} |
|||
Loading…
Reference in new issue