Skip to content

Commit 9b7de5c

Browse files
perf(ui): use startTransition for dimension changes instead of RAF batch
The previous RAF-based dimension batching collected all dimension changes and flushed them in a single commit. Profiling showed this created a 190ms jank (commit #55) — worse than the original 8 × ~20ms spread across multiple frames. Replace with a simpler approach: wrap dimension changes in React.startTransition so React schedules them at lower priority. This avoids concentrating all dimension work into one frame while still keeping interactive changes (select, drag, remove) immediate. Also keeps the merged startTransition for setNodes+setEdges in applyPatch (from the previous commit) which avoids double renders from the subscription path. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
1 parent 88ea511 commit 9b7de5c

File tree

1 file changed

+10
-31
lines changed

1 file changed

+10
-31
lines changed

ui/src/views/MonitorView.tsx

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1494,16 +1494,13 @@ const MonitorViewContent: React.FC = () => {
14941494
const [nodes, setNodes, onNodesChangeInternal] = useNodesState<RFNode>([]);
14951495
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
14961496

1497-
// ── Debounce ReactFlow dimension changes ──────────────────────────────
1498-
// When ReactFlow measures newly-mounted nodes it fires onNodesChange
1499-
// with 'dimensions' changes — one per node, across multiple frames.
1500-
// Each call updates the nodes array in MonitorViewContent's state,
1501-
// triggering a full ~20 ms re-render. We debounce dimension changes
1502-
// into a single RAF callback while applying interactive changes
1503-
// (selection, drag, remove) immediately.
1504-
const pendingDimChanges = useRef<NodeChange[]>([]);
1505-
const dimRafRef = useRef<number | null>(null);
1506-
1497+
// ── Low-priority dimension changes ────────────────────────────────────
1498+
// ReactFlow fires onNodesChange with 'dimensions' type for each node
1499+
// after mount measurement. These are internal bookkeeping (the nodes
1500+
// are already visible) so we wrap them in startTransition to let React
1501+
// schedule them at lower priority rather than blocking the main thread.
1502+
// Interactive changes (select, drag, remove) bypass this and apply
1503+
// immediately.
15071504
const onNodesChangeBatched = useCallback(
15081505
(changes: NodeChange[]) => {
15091506
const immediate: NodeChange[] = [];
@@ -1517,26 +1514,14 @@ const MonitorViewContent: React.FC = () => {
15171514
}
15181515
}
15191516

1520-
// Interactive changes (select, drag, remove) apply immediately
15211517
if (immediate.length > 0) {
15221518
onNodesChangeInternal(immediate);
15231519
}
15241520

1525-
// Dimension changes are deferred and batched into one RAF
15261521
if (deferred.length > 0) {
1527-
pendingDimChanges.current.push(...deferred);
1528-
if (dimRafRef.current === null) {
1529-
dimRafRef.current = requestAnimationFrame(() => {
1530-
dimRafRef.current = null;
1531-
const batch = pendingDimChanges.current;
1532-
pendingDimChanges.current = [];
1533-
if (batch.length > 0) {
1534-
React.startTransition(() => {
1535-
onNodesChangeInternal(batch);
1536-
});
1537-
}
1538-
});
1539-
}
1522+
React.startTransition(() => {
1523+
onNodesChangeInternal(deferred);
1524+
});
15401525
}
15411526
},
15421527
[onNodesChangeInternal]
@@ -3047,12 +3032,6 @@ const MonitorViewContent: React.FC = () => {
30473032
return () => {
30483033
unsubscribe();
30493034
if (throttleTimer !== null) clearTimeout(throttleTimer);
3050-
// Cancel any pending dimension-change RAF from onNodesChangeBatched
3051-
if (dimRafRef.current !== null) {
3052-
cancelAnimationFrame(dimRafRef.current);
3053-
dimRafRef.current = null;
3054-
pendingDimChanges.current = [];
3055-
}
30563035
};
30573036
}, [selectedSessionId, setNodes, setEdges]);
30583037

0 commit comments

Comments
 (0)