Visual Workflow Builder — ReactFlow Graph SaaS
Enterprise SaaS · React 18 · ReactFlow · TypeScript
The Challenge
An enterprise SaaS platform needed a visual workflow builder that allowed non-technical users to design complex, branching business process flows using a drag-and-drop node graph interface. The existing implementation, built on ReactFlow, degraded to below 10 FPS when graphs exceeded 300 edges — making it unusable for the power users who needed 500–2,000+ node workflows.
The performance degradation had a clear root cause: the entire React component tree was re-rendering on every node drag event. With 500 nodes, this meant thousands of component evaluations every 16ms — a fundamental misuse of React's reconciler. The problem was architectural, not cosmetic.
The business risk was significant: enterprise clients evaluating the product immediately opened their largest existing workflows to test it. The sub-10 FPS experience was disqualifying every trial conversion.
The Approach
We began with Chrome DevTools Profiler and React DevTools to establish a precise baseline and identify the exact re-render waterfall. The diagnosis confirmed that ReactFlow's state was stored in a shared React Context, causing every state update — including node position during drag — to propagate to every node component simultaneously.
The remediation followed three sequential phases: first, migrating ReactFlow's internal state out of Context and into a Zustand store with transient subscriptions that bypassed React's render cycle during drag operations. Second, wrapping every custom node component with React.memo and strict prop equality checks to prevent re-renders when unrelated nodes updated. Third, migrating all graph data fetching to Apollo Client with normalized cache, eliminating full graph refetches on partial data changes.
Each phase was profiled independently to verify its contribution before the next was implemented.
What We Built
Zustand State Decoupling
ReactFlow's internal node and edge state migrated from React Context to Zustand's transient update API. Node position changes during drag operations now bypass React's reconciler entirely — zero component re-renders during drag.
React.memo Node Optimization
Custom node components wrapped with React.memo and shallow-equality checks on all props. Updating one node's data no longer triggers re-renders in the other 1,999 nodes on the canvas.
Apollo Normalized Cache
Graph data loading migrated to Apollo Client with a normalized cache, allowing partial data updates to resolve at the entity level — no full graph refetch required when a single node's configuration changes.
Canvas Virtualization
ReactFlow's built-in viewport virtualization configured and tuned to render only nodes within the current viewport bounding box, reducing DOM nodes proportionally to the visible area regardless of total graph size.
Special features that closed the deal.
- Zustand transient state subscriptions for ReactFlow — node position updates during drag bypass React's render cycle entirely, achieving zero re-renders during canvas interaction
- React.memo with custom equality functions on all custom node types — updating one node does not trigger re-renders in sibling nodes
- Apollo Client normalized entity cache — individual node configuration changes resolve without refetching the full graph payload
- Sustained 40–56 FPS on 2,000+ node graphs on standard enterprise hardware — up from below 10 FPS on the original implementation
Outcomes
Sustained frame rate on dense graphs — up from under 10 FPS
Interconnected nodes rendered without performance degradation
Reduction in React component re-renders on node drag operations
Technologies used