The ultimate guide to optimize React Flow project performance [E-BOOK]

Łukasz Jaźwa
Jan 23, 2025
2
min read

Follow these step-by-step process to enhance your React Flow project's performance.

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:

  1. Every node's position change causes node to re-render.
  1. A node's state change causes a refresh of ReactFlow's internal state.
  1. ReactFlow's internal state refreshing leads to the <ReactFlow> 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:

A code presenting a basic usage of <ReactFlow> component

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:

A code presenting a change in nodes loading time after adding to <ReactFlow> component an 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:

A code presenting unoptimial way to show selected nodes' IDs.

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.

A code presenting an more efficient way to show nodes IDs that avoid unecessary rendering.
A code presenting an more efficient way to show nodes IDs that avoid unecessary rendering.

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).

A code presenting memoization of the array of primitive types.

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:

A code that restores an anonymous function in<ReactFlow> component.

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

A code that wraps up a Node component in React.memo.

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.  

It is worth noting that 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.

The conclusion  

  • Every custom template should be wrapped in a React.memo, which significantly decreases the burden during operations such as dragging.

“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.  

A code that uses React.memo for nodes with "heavy" content.

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:

A code presenting a natural approach for getting data from Zustand store.

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.

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

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.

A code creating a store that use the function createWithEquityFn with a shallow parameter.

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

A code presenting an original approach to useStore hooks.

Read the full version

Fill the form below to get an e-book where you'll find the full version of this guide with two more hints on optimizing the React Flow project's performance and bonus contents on: 

  • How to debug your React Flow project.
  • How to identify performance bottlenecks.
Contact details
By sending a message you allow Synergia Pro Sp. z o.o., with its registered office in Poland, Wroclaw (51-607) Czackiego Street 71, to process your personal data provided by you in the contact form for the purpose of contacting you and providing you with the information you requested. You can withdraw your consent at any time. For more information on data processing and the data controller please refer to our Privacy policy.
*Required
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Łukasz Jaźwa
CTO

CTO at Synergy Codes who ensures that new customers are stunned with our code, quality and knowledge. He is also a great GoJS expert who loves theoretical side of computer science.

Get more from me on:
Share:

Articles you might be interested in

10 tips for better initial configuration of full-stack apps

Follow these tips for configuring full-stack apps, from using a monorepo with Nx to setting up code formatting and CI/CD pipelines, ensuring smooth development.

Tomasz Świstak
Aug 12, 2022

Effective front-end development with GoJS

Learn how GoJS enhances front-end development by creating interactive diagrams with flexibility and performance for complex data visualizations.

Content team
Oct 1, 2021

Angular vs. React: Which technology is more efficient?

Compare the performance of Angular and React in large apps, focusing on memory usage and optimization needs. Learn when to choose each in the upcoming webinar.

Kacper Cierzniewski
Aug 4, 2021