Browse Source

Revert "perf: cap RAF-based callbacks to 60fps on high refresh rate displays"

This reverts commit 9f25e05d87.
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
703fcf567a
  1. 11
      src/components/nodes/FloatingNodeHeader.tsx
  2. 8
      src/components/nodes/ModelParameters.tsx
  3. 11
      src/store/workflowStore.ts
  4. 75
      src/utils/throttledRAF.ts

11
src/components/nodes/FloatingNodeHeader.tsx

@ -7,7 +7,6 @@ import { NodeType, ProviderType } from "@/types";
import { useWorkflowStore } from "@/store/workflowStore";
import { defaultNodeDimensions } from "@/store/utils/nodeDefaults";
import { ProviderBadge } from "./ProviderBadge";
import { createThrottledLoop } from "@/utils/throttledRAF";
export interface CommentNavigationProps {
currentIndex: number;
@ -122,11 +121,15 @@ export const FloatingNodeHeader = memo(function FloatingNodeHeader({
updatePosition();
const loop = createThrottledLoop(updatePosition);
loop.start();
let animationId: number;
const trackPosition = () => {
updatePosition();
animationId = requestAnimationFrame(trackPosition);
};
animationId = requestAnimationFrame(trackPosition);
return () => {
loop.stop();
cancelAnimationFrame(animationId);
};
}, [showCommentTooltip, isCommentFocused]);

8
src/components/nodes/ModelParameters.tsx

@ -5,7 +5,6 @@ import { ProviderType, ModelInputDef } from "@/types";
import { ModelParameter } from "@/lib/providers/types";
import { useProviderApiKeys } from "@/store/workflowStore";
import { deduplicatedFetch } from "@/utils/deduplicatedFetch";
import { createThrottledRAF } from "@/utils/throttledRAF";
// localStorage cache for model schemas (persists across dev server restarts)
const SCHEMA_CACHE_KEY = "node-banana-schema-cache";
@ -197,16 +196,17 @@ function ModelParametersInner({
useEffect(() => {
const el = gridRef.current;
if (!el || !useGrid) { setColCount(1); return; }
const throttle = createThrottledRAF();
let rafId: number;
const observer = new ResizeObserver(() => {
throttle.schedule(() => {
cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
const cols = getComputedStyle(el).gridTemplateColumns.split(" ").length;
setColCount(prev => prev === cols ? prev : cols);
});
});
observer.observe(el);
return () => {
throttle.cancel();
cancelAnimationFrame(rafId);
observer.disconnect();
};
}, [useGrid]);

11
src/store/workflowStore.ts

@ -62,7 +62,6 @@ import {
import { getConnectedInputsPure, validateWorkflowPure } from "./utils/connectedInputs";
import { evaluateRule } from "./utils/ruleEvaluation";
import { computeDimmedNodes } from "./utils/dimmingUtils";
import { createThrottledRAF } from "@/utils/throttledRAF";
import {
executeAnnotation,
executeArray,
@ -383,9 +382,9 @@ let nodeIdCounter = 0;
let groupIdCounter = 0;
let autoSaveIntervalId: ReturnType<typeof setInterval> | null = null;
// Throttled RAF for hover updates — coalesces rapid mouseenter/mouseleave events
// and caps at 60fps regardless of display refresh rate
const hoverRAF = createThrottledRAF();
// RAF debounce for hover updates — coalesces rapid mouseenter/mouseleave events
// into a single store update per animation frame
let hoverRafId: number | null = null;
// Track pending save-generation syncs to ensure IDs are resolved before workflow save
const pendingImageSyncs = new Map<string, Promise<void>>();
@ -531,7 +530,9 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
},
setHoveredNodeId: (id: string | null) => {
hoverRAF.schedule(() => {
if (hoverRafId !== null) cancelAnimationFrame(hoverRafId);
hoverRafId = requestAnimationFrame(() => {
hoverRafId = null;
if (get().hoveredNodeId !== id) set({ hoveredNodeId: id });
});
},

75
src/utils/throttledRAF.ts

@ -1,75 +0,0 @@
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…
Cancel
Save