Skip to content
  • Place to introduce & feedback about this space

    4 Topics
    12 Posts
    D

    Hi all! 👋
    My name is Mateusz, and I’m a Software Developer with over 6 years of experience working in data visualization and diagramming at Synergy Codes.
    Feel free to reach out if you have questions, need advice, or want to collaborate. 😁

  • Products & Projects related with data vis

    4 Topics
    4 Posts
    PMarcinP

    JavaScript diagram library built on four foundations: Design System, UI library, Diagram library, AI for diagrams. For more information, visit https://www.vislabs.ai/

    Feel free to share your opinions and thoughts

  • Discousion and tech hub

    1 Topics
    1 Posts
    lukasz.jazwaL
    Table of Contents Introduction React Flow component optimization Dependencies on node and edge arrays Custom nodes and edges templates "Heavy" nodes Access to Zustand store Memoization with useShallow Function createWithEqualityFn UI Components Zustand store dependency <ReactFlow> child components Application's architecture Presentational and container components pattern Application's state Bonus: How to do debugging Performance analysis Identifying bottlenecks

    Why is React Flow prone to performance issues?

    React Flow is a library vulnerable to a diagram’s performance pitfalls. Especially when a developer isn't cautious while coding.

    Even one non-optimized line of code can cause unnecessary re-rendering of the diagram's elements on every state change (mainly when the positions of nodes are updated). As a result, an application works slowly.

    To understand how easily performance issues can arise, let’s analyze the following example. When you drag a node on the diagram, the application acts as follows:

    Every node's position change causes node to re-render.

    A node's state change causes a refresh of ReactFlow's internal state.

    ReactFlow's internal state refreshing leads to the component refreshing, which relies, among other things, on the nodes array. Consequently, any changes to a node or other diagram element cause the re-rendering of this main component.

    Re-rendering of a single component usually doesn't impact an application's performance.

    However, if we unintentionally make the states of nodes dependent on each other or place heavy components as children within the main ReactFlow component, the application may lose its smoothness and fail to meet performance requirements.

    In addition, there is a general rule in the React ecosystem not to optimize code too early unless there are performance issues.

    As for React Flow, if such issues occur later in a development process, they can be difficult to overcome without significant changes in code logic.

    So, the sooner you work on software performance optimization, the better.

    Now, I will share five hints on avoiding performance pitfalls in React Flow applications. I based all of them on a tested project’s performance audit and my over 11 years of experience building diagrams in multiple visual applications.

    The data I present in this article comes from a project consisting of 100 nodes. Every node had two Handles and rendered one of two things:

    In default mode, one text input from MaterialUI. In "heavy" mode, one DataGrid from MaterialUI that had nine rows and five columns.

    The base FPS (frames per second) with optimal performance in the project on my computer is 60 FPS.

    #1. <ReactFlow> component optimization

    The basic usage of the <ReactFlow> component is as follows:

    <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} />

    The React Flow library's documentation strongly recommends passing memoized references to this component, whether the props are functions or objects. To ensure optimal performance, follow these two rules:

    Objects memoization: Objects passed to the <ReactFlow> component should be memoized using useMemo or defined outside of the component.

    Functions memoization: All functions passed as props should be memoized using useCallback.

    Additionally, memoized objects and functions must have stable dependencies to ensure consistent behavior.

    If you include elements with frequently changing references (e.g., functions not wrapped in useCallback) in the dependencyArray of useMemo or useCallback, the memoization will not yield the expected results.

    Benchmark

    Let’s consider the following code modification that illustrates this issue:

    <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} onNodeClick={() => {}} //added anonymous function />

    Introducing an anonymous function to onNodeClick prop forces React to assign a new reference in every render.

    The results (number of FPS) for dragging operation:

    100 default nodes: Decrease to 10 FPS.

    100 "heavy" nodes: Decrease to 2 FPS.

    The cause

    This change caused the re-render of all the diagram's nodes whenever a node's state was updated. It means that with every dragging operation, not only the main <ReactFlow> component and dragged node are being re-rendered but also 99 nodes remaining.

    The conclusion

    You must remember to properly memoize props, i.e., using useCallback and useMemo, when working with <ReactFlow> component.

    #2. Dependencies on Node and Edge Arrays

    Uncontrolled dependencies of components and hooks on node and edge arrays are among the main threats to the performance of applications using ReactFlow.

    The state of nodes and edges can change even with minor updates to individual properties of any diagram element. This often results in unnecessary re-renders of components dependent on this state.

    The example

    Let's assume that you want to display selected nodes' IDs. A quick but not optimal way is:

    export const Node: React.FC<NodeProps<BaseNode>> = () => { const selectedNodes = useStore( (state) => state.nodes.filter((node) => node.selected) ); return ( <div className="react-flow__node-default"> <Handle type="target" position={Position.Top} /> <div className={styles.nodeContainer}> <DefaultContent /> {selectedNodes.map((node) => node.id)} </div> <Handle type="source" position={Position.Bottom} id="a" /> </div> ); };

    We fetch the complete node array from the store, filter the selected objects, and display their IDs inside the nodes

    The results (number of FPS) for dragging operation:

    100 default nodes: Decrease to 12 FPS.

    100 “heavy” nodes: Decrease to 2 FPS.

    The cause

    The main issue arises from the behavior of the useStore hook: the selectedNodes reference changes with every update of state.nodes (e.g., during every tick of a dragging operation). As a result, all nodes on the diagram defined by this component will re-render with every state update, regardless of whether they are being dragged or not.

    The solution

    To avoid unnecessary rendering, you can define a field in store where you can keep selected objects. Thanks to that a Node component won’t be directly dependent on nodes array.

    //Store const useStore = .... .... selectedNodes: [], setSelectedNodes: (selectedNodes: Node[]) => { set({ selectedNodes }); }, ... })); // // Diagram const [ nodes, edges, onNodesChange, onEdgesChange, onConnect, setSelectedNodes, ] = useStore( useShallow((state) => [ state.nodes, state.edges, state.onNodesChange, state.onEdgesChange, state.onConnect, state.setSelectedNodes, ]) ); const onSelectionChange = useCallback( (event: OnSelectionChangeParams) => { setSelectedNodes(event.nodes); }, [setSelectedNodes] ); return ( <div className={styles['diagram']}> <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} onSelectionChange={onSelectionChange} /> </div> ); // // Node component export const Node: React.FC<NodeProps<BaseNode>> = () => { const selectedNodes = useStore((state) => state.selectedNodes); return ( <div className="react-flow__node-default" > <Handle type="target" position={Position.Top} /> <div className={styles.nodeContainer}> <DefaultContent /> {selectedNodes.map((node) => node.id)} </div> <Handle type="source" position={Position.Bottom} id="a" /> </div> ); };

    In this approach, the selectedNodes variable is refreshed if and only if the selection actually changes, avoiding unnecessary re-renders of the components that depend on it.

    An alternative approach

    When we want to extract an array of primitive types from a collection of nodes or edges, as shown in the example above, there is also a straightforward solution that uses shallow comparison provided by Zustand.

    Zustand offers methods for memoizing selectors either by using the useShallow hook or by creating a store with createWithEqualityFn and passing the shallow parameter. In this example, we use a solution based on store configuration with createWithEqualityFn (see more in Zustand chapter).

    const selectedNodes = useStore( (state) => state.nodes.filter((node) => node.selected).map((node) => node.id) );

    Thanks to this approach, the array of primitive types is memoized. Even if its reference changes, the selector will still return the previous reference as long as none of the elements in the array have changed.

    The conclusions

    Using state from the ReactFlow store in components should be carefully considered, especially when there are dependencies on dynamically changing arrays of nodes or edges.

    It's easy to notice issues with suboptimal state usage in a custom node component. However, similar issues can also arise in other scenarios:

    "Heavy" UI component's dependency on nodes and edges array: Let's take an example. Suppose you have a sidebar with MaterialUI forms that depend on the nodes array. In that case, this sidebar will render every time any diagram's object changes, decreasing performance.

    Hook dependency on nodes and edges array: For instance, a hook that depends on the nodes array and whose returned value is included in the dependencyArray of a function passed to <ReactFlow>. In this scenario, simply using useCallback won't solve the issue because the hook still changes its reference based on dynamically changing data. As a result, the returned values will be updated just as frequently.

    #3. Custom nodes and edges templates

    One of the most effective ways for keeping a ReactFlow app’s performance is wrapping custom nodes and edges in React.memo.

    Why use React.memo?

    Thanks to wrapping nodes and edges in a React.memo component, even if listeners on the main ReactFlow component are not used optimally, smaller diagrams are unlikely to experience significant performance issues. This is because the contents of nodes and edges won't re-render during i.e. dragging operation.

    Benchmark

    Let's test how wrapping in memo impacts performance. To do that, let's restore an anonymous function in the main <ReactFlow> component:

    <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} onSelectionChange={onSelectionChange} onNodeClick={() => {}} />

    And let’s wrap Node component in React.memo:

    export const Node: React.FC<NodeProps<BaseNode>> = memo(() => { const selectedNodes = useStore((state) => state.selectedNodes); return ( <div className="react-flow__node-default"> <Handle type="target" position={Position.Top} /> <div className={styles.nodeContainer}> <DefaultContent /> {selectedNodes.map((node) => node.id)} </div> <Handle type="source" position={Position.Bottom} id="a" /> </div> ); });

    The results (number of FPS) for dragging operation:

    100 default nodes: In the first second of operation, the number decreases to 50 FPS, then becomes stable at 60 FPS.

    100 "heavy" nodes: Decrease to 30 FPS.

    When compared to the first chapter's test's result without using memo (decrease to 10 FPS for default nodes and to 2 FPS for "heavy" nodes), it is clearly visible that wrapping components in memo significantly improves performance.

    I conducted these tests with developer tools turned off in my browser. These tools can slow down an application and negatively impact the FPS count.

    “Heavy” nodes

    Tests prove that “heavy” nodes (those with more complex components, such as DataGrid from MaterialUI) have a bigger impact on a diagram’s performance.

    Having implemented all above-mentioned optimization, the results (number of FPS) for dragging operation present as follows:

    100 “heavy” nodes: Decrease to 35-40 FPS. The solution

    To minimize the impact that Node’s contents have on performance, you can wrap the inside elements in React.memo component. Thanks to that, their rendering will be limited only to these cases, when their props will actually change.

    export const Node: React.FC<NodeProps<BaseNode>> = memo(() => { const selectedNodes = useStore((state) => state.selectedNodes); return ( <div className="react-flow__node-default"> <Handle type="target" position={Position.Top} /> <div className={styles.nodeContainer}> <HeavyContent /> {selectedNodes.map((node) => node.id)} </div> <Handle type="source" position={Position.Bottom} id="a" /> </div> ); }); // using React.memo for heavy content const HeavyContent = memo(() => ( <DataGrid rows={rows} columns={columns} initialState={{ pagination: { paginationModel: { pageSize: 5, }, }, }} pageSizeOptions={[5]} checkboxSelection disableRowSelectionOnClick /> ));

    The results (number of FPS) for dragging operation:

    100 “heavy” nodes: In the first second of operation, the number decreases to 35-40 FPS, then becomes stable at 60 FPS. The conclusions

    If a node has heavy content, such as DataGrid from MaterialUI, the content should be wrapped in React.memo. This will avoid unnecessary re-rendering of its contents during state’s updates.

    #4. Access to Zustand store

    Zustand is a small, fast and flexible state management library for React applications, used internally by React Flow.

    While getting data from the Zustand store, a natural approach would be writing such a code:

    const [selectedNodes, someOtherProperty] = useStore((state) => [ state.selectedNodes, state.someOtherProperty, ]);

    Although this code seems intuitive, without a proper store configuration, it can lead to performance issues and, in some cases, errors such as Maximum update depth exceeded.

    This happens because the array returned by useStore is recreated from scratch every time the state changes. Since the resulting array has a different reference on each update, it causes component to re-render even if individual values within the array remain unchanged. A similar situation occurs if an object is returned instead of an array — every state change generates a new reference to the object.

    Memoization with useShallow

    Zustand provides the useShallow hook, which memoizes the returned reference if the contents of an array or object have not changed, helping reduce unnecessary re-renders.

    const [selectedNodes, someOtherProperty] = useStore(useShallow((state) => [ state.selectedNodes, state.someOtherProperty, ]));

    The drawback of this method is that useShallow requires remembering to use it every time you get more than one field from the store.

    Function createWithEqualityFn

    The alternative method is to create a store using the function createWithEquityFn with a shallow parameter. As a result, every selector uses memoization with shallow comparison by default.

    const useStore = createWithEqualityFn<AppState>( (set, get) => ({ ... }), shallow );

    Thanks to that, you can use the original approach to useStore hooks.

    const [selectedNodes, someOtherProperty] = useStore((state) => [ state.selectedNodes, state.someOtherProperty, ]);

    #5. UI components

    Regular React components, apart from diagram objects, can also negatively impact performance if they are not properly optimized.

    In such cases, you should follow the hint I explained in the previous chapter and a chapter dedicated to dependencies on node and edge arrays

    Zustand store dependency

    Let’s look at a sidebar with a form that displays IDs of selected nodes.

    export const Sidebar = memo(() => { const selectedNodes = useStore((state) => state.nodes.filter((node) => node.selected) ); return ( <div className={styles.container}> <div>Selected object key: {selectedNodes.map((node) => node.id)}</div> <TextField label="Attribute 1" defaultValue="Value 1" /> <TextField label="Attribute 2" defaultValue={'Value 2'} /> ...

    In this example, a Sidebar will re-render every time there’s a change in the nodes array. For instance, at every dragging operation, because selectedNodes reference changes at every state.nodes update.

    You can optimize this component using methods from previous chapters of this article. Assuming that the store was created with createWithEqualityFn with shallow parameter, you can memoize selectors' results.

    export const Sidebar = memo(() => { // The selector extracts an array of IDs of selected nodes, which is memoized by Zustand // thanks to the use of createWithEqualityFn when creating the store const selectedNodeIds = useStore((state) => state.nodes.filter((node) => node.selected).map((node) => node.id) ); return ( <div className={styles.container}> <div>Selected object keys: {selectedNodeIds}</div> <TextField label="Attribute 1" defaultValue="Value 1" /> <TextField label="Attribute 2" defaultValue={'Value 2'} /> ...

    Zustand memoizes the selector's result, and selectedNodeIds will change its reference only if the selection actually changes, which Zustand's shallow comparison will detect.

    Previous selector result wasn't memoized because it extracted nodes' references from the state, which changes much more often than selected IDs.

    <ReactFlow> child components

    Nested components placed as children of the main <ReactFlow> component will be automatically re-rendered with every state change in the diagram because <ReactFlow> itself re-renders that often. To prevent this, they should be wrapped in React.memo.

    ... return ( <div className={styles['diagram']}> <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} onSelectionChange={onSelectionChange} > <ChildComponent /> </ReactFlow> </div> ); } // child component wrapped in React.memo const ChildComponent = memo(() => { ... });

    #6. Application’s architecture

    Let’s focus on what you should consider for your application’s architecture to avoid some of the performance issues while working with React Flow and to make it easier to identify potential performance bottlenecks.

    Presentational and Container Components pattern

    This pattern helps to isolate performance issues and makes them easier to identify.

    Presentational components:

    They are responsible only for displaying data passed by props.

    If they have a state, it is only local and independent from the global application’s state.

    They are light and easy to unit test.

    Container components:

    Have access to an app's state (e.g., to Zustand store).

    Define business logic and interactions (e.g., data processing, functions calls, communication with backend).

    Use hooks.

    Pass data and functions as props to presentational components.

    By the above definition, performance issues are most often found in Container Components, which means you usually don’t need to review Presentational Components when looking for the source of the problem.

    Application’s state

    In ReactFlow applications, due to the high volatility of nodes' and edges' states, I recommend avoiding storing the diagram state in useState, useReducer, or the Context API. Instead, it’s best to move the state to a Zustand store.

    Zustand allows precise subscription to selected parts of the state and performance optimization through selector memoization.

    Bonus: How to do debugging?

    You already know some hints on how to impact React Flow apps' performance. Now, let’s see how to check if your app has any performance issues.

    Performance analysis

    I usually use React Developer Tools, especially the Profiler module, to analyze an app's performance. You can find it (after installation) in your browser in Developer Tools.

    image-20241126-073559.png

    My workflow of analyzing with Profiler looks as follows:

    Start an application and add several dozen nodes on a diagram (e.g., 30).

    Turn on recording in Profiler.
    image-20241126-073734.png

    Choose one node and drag it for a few seconds.

    Stop the recording in Profiler.
    image-20241126-073844.png

    Go to the Flamegraph section.
    image-20241126-074449.png

    How to interpret results on Flamegraph:

    Dark-grey elements are components that haven’t been rendered.

    Vertical, narrowing down “teeth” under the NodeRenederer group represent node components. If you hover over any node, it highlights it in an application, making it easy to identify.

    An optimized application will highlight only one node on a graph (the dragged one), while a not optimized one will display many elements in the same frame. You can change frames in Profiler’s right upper corner—on a colorful bar chart.

    Below, you can see a Flamegraph of an application that wasn’t optimized. In a rendering frame, you can observe many more highlighted nodes. This means that while one of them was dragged, the rest also re-rendered.

    image-20241126-075225.png

    In a similar way, you can look for not optimized UI components.

    The main goal is to have a case in which, while moving only one element, only this element and its edges are re-rendered (unless business requirements dictate different behavior).

    Identifying bottlenecks

    There’s no one perfect way to identify bottlenecks, but my approach is as follows:

    Analyzing communication with a store. Check if selectors are memoized and if a state is effectively managed (e.g., with a Zustand). Analyzing references passed to <ReactFlow>. Make sure that you use useCallback and useMemo and check their dependencies in dependencyArray. Nodes and edges memoization. Check if nodes and edges are wrapped in React.memo. In less complex projects: Review of using state in application. Check manually all state usage according to good practices. In complex projects: Perform a binary search for <ReactFlow> component. Comment out half of the props passed to <ReactFlow> and check if it improved performance. If so, uncomment half of the props and search further. If not, comment out half of the remaining props and search further. Nodes and edges contents: Comment out their contents to check if the source of performance issue lies in rendered elements’ complexity. Use binary search, as I mentioned above. Check the usage of 3rd party libraries in components. The application’s main panels: Comment out everything besides a diagram (and eventually search for an issue with a binary search).

    Sometimes, a bottleneck hides in one place, and one of the above points will help. However, there might be also several bottlenecks and then you have to combine some of those techniques.

    Wrap up

    I hope that this tutorial will help you improve performance in your React Flow application. If you have additional questions, don't hesitate to reply here.

  • Discousion and Tech Hub

    8 Topics
    8 Posts
    KubaSCK
    Problem 🐛

    In GoJS, when we try to perform a copy-paste operation on partially or fully disconnected links, they are not pasted onto the diagram.

    11.png

    Solution

    To enable pasting of such links, set the property:

    diagram.toolManager.draggingTool.dragsLink = true;

    This property allows links to be dragged, which may not be desirable in the project. To ensure that only copy/paste works, simply add the following code in the CommandHandler:

    override pasteFromClipboard(): go.Set<go.Part> { const oldDragsLink = this.diagram.toolManager.draggingTool.dragsLink; this.diagram.toolManager.draggingTool.dragsLink = true; const pastedParts = super.pasteFromClipboard(); this.diagram.toolManager.draggingTool.dragsLink = oldDragsLink; }

    After overriding this method, both partially and fully disconnected links should be correctly pasted onto the diagram. 🎉

    2.png

  • Lets talk about ML (AI)

    1 Topics
    1 Posts
    PMarcinP

    Based on interviews and a survey of diagrammatic experts - we have prepared a report.
    Direct link here.
    If you have any suggestions or questions - please write boldly

    de0f65a8-dcb8-4940-8e4f-f5cd0c8f39a6-image.png