DEV Community

Cover image for # We Replaced React Flow in Our App and Cut the Diagram Bundle from 60.5 kB to 19 kB
Naveen Boddepalli
Naveen Boddepalli

Posted on AI-assisted

# We Replaced React Flow in Our App and Cut the Diagram Bundle from 60.5 kB to 19 kB

How we built a lightweight, zero-runtime-dependency React graph library with a familiar API, modular architecture, free open-source editing utilities, and 1,000+ compatibility-test assertions.


If your React application renders node-based graphs—workflow builders, ETL pipelines, database query plans, syntax trees, or org charts—you’ve probably come across React Flow (@xyflow/react).

It’s one of the most established libraries in the React graph ecosystem. It’s mature, well-documented, and packed with features.

But not every application needs a full graph-editing framework.

A few months ago, while building QueryLens, a developer tool for visualizing SQL query execution plans, we ran into a problem: our diagram viewer was shipping more JavaScript than we wanted for a relatively focused visualization feature.

The diagram chunk in our client build weighed over 60.5 kB gzipped. Along with the rendering logic, we were bringing in third-party dependencies for state management, gestures, and graph interactions, plus a mandatory global stylesheet import in our integration.

For an application primarily designed to display query plans, we wanted something leaner.

We didn’t want to sacrifice familiar APIs, custom node components, connected edges, or interactive navigation. We wanted to remove unnecessary overhead while preserving the developer experience.

So we built Flowinger: a lightweight React node-graph library designed to minimize migration effort while offering a modular architecture and zero external runtime dependencies beyond its React peer dependencies.

In our QueryLens build:

  • The diagram chunk dropped from 60.5 kB to 19 kB gzipped (a ~68.6% reduction).
  • In our 10,000-node, unculled benchmark, Flowinger mounted the graph in 0.7 seconds compared with 1.3 seconds for React Flow (~1.9× faster).
  • We built a collection of MIT-licensed utilities for undo/redo, clipboard operations, canvas export, alignment guides, and advanced edge rendering.

Here’s how we approached the architecture, what our benchmarks show, where React Flow still wins, and what developers should know before switching.


1. What We Kept: A Familiar API and a Minimal Migration Path

The biggest obstacle to adopting an alternative graph library isn’t always performance. It’s migration cost.

Developers may already have dozens of custom node components, edge renderers, connection callbacks, layout algorithms, and state-management integrations built around React Flow. Rewriting all of that just to save a few kilobytes isn’t an attractive proposition.

We designed Flowinger around a simple principle:

Preserve React Flow’s API patterns wherever practical, so developers can evaluate an alternative without rewriting their entire graph implementation.

Our compatibility target is @xyflow/react 12.11.6. The project includes implementations of its documented exports and props, together with automated tests for selected behavioral and interaction contracts.

For a typical integration, the import change looks like this:

- import { ReactFlow, Background, Controls, useReactFlow } from '@xyflow/react';
- import '@xyflow/react/dist/style.css';
+ import { ReactFlow, Background, Controls, useReactFlow } from 'flowinger';
Enter fullscreen mode Exit fullscreen mode

The stylesheet import disappears, too.

Flowinger doesn’t ship a mandatory global CSS file. Nodes, handles, controls, and edges use inline styling and CSS variables, including the --flowinger-* namespace. We also preserve standard .react-flow__* class names in relevant components to make migration easier for applications that already use those selectors.

However, preserving class names doesn’t guarantee complete stylesheet compatibility. Applications that depend heavily on React Flow’s default stylesheet or --xy-* variables may need some re-theming.

Similarly, matching exports and prop names doesn’t guarantee identical behavior in every edge case. We recommend testing custom nodes, edge interactions, keyboard behavior, and event handlers before migrating a production application.

The goal is straightforward: make migration small enough that trying Flowinger becomes a reasonable engineering decision.


2. How It’s Smaller: Reducing Runtime Overhead Without Abandoning Functionality

How do you reduce the JavaScript shipped for a graph visualization while retaining the features developers need? We focused on two architectural decisions:

Zero External Runtime Dependencies

Flowinger has zero external runtime dependencies beyond its React peer dependencies (react and react-dom).

Its pan, zoom, pointer-interaction, and geometry logic are implemented without requiring separate runtime packages for those capabilities. This reduces the number of external packages required by the graph library and gives us more control over the rendering and interaction paths.

It also reduces the dependency surface developers need to evaluate when adding Flowinger to an existing application.

A Modular Core

Not every graph needs the same functionality.

A SQL query-plan viewer may need rendering, navigation, and selection. A workflow editor may also need dragging, connection creation, undo/redo, and alignment guides. Bundling every optional capability into every integration works against the goal of keeping a visualization lightweight.

Flowinger therefore provides a core rendering path with optional features:

import { FlowingerCore, dragFeature, selectionFeature } from 'flowinger';

function GraphViewer({ nodes, edges }) {
  return (
    <FlowingerCore
      nodes={nodes}
      edges={edges}
      features={[dragFeature, selectionFeature]}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

This lets applications explicitly opt into the capabilities they need.

Flowinger also provides a higher-level ReactFlow component, aliased to FlowingerCanvas, for applications that want a more complete canvas experience.

The full canvas and the modular core serve different use cases. The core can produce a smaller bundle when optional functionality is excluded, while the higher-level canvas offers a more convenient integration path.

Actual bundle sizes depend on the entry point, application code, bundler configuration, and tree-shaking effectiveness. Unused features can be excluded when the package and application are configured appropriately.

For our QueryLens integration, the resulting production diagram chunk was 19 kB gzipped, compared with 60.5 kB for the React Flow implementation. That figure describes our application build, not the size of the entire Flowinger package or every possible Flowinger integration.


3. The Numbers: Benchmarks and Real Application Bundles

We benchmarked Flowinger and React Flow using synthetic grid graphs ranging from 1,000 to 10,000 nodes. The tests use Chrome and Playwright, including mouse and wheel interactions.

The benchmark suite and methodology are available in the Flowinger benchmark documentation.

Here are our reported benchmark results:

Metric Flowinger React Flow (@xyflow/react 12)
External runtime dependencies 0 (only React peers) Multiple dependencies (zustand, D3 modules…)
Mandatory global stylesheet No (CSS variables) Yes (@xyflow/react/dist/style.css)
QueryLens diagram chunk (gzipped) 19.0 kB 60.5 kB
Mount time (10,000 nodes, unculled) 0.7 s 1.3 s
Pan time (10,000 nodes, unculled) 10.6 ms 35.0 ms
Zoom time (10,000 nodes, unculled) 12.0 ms 36.0 ms
Pan time (10,000 nodes, culled) 4.4 ms 3.1 ms (React Flow is faster)
Zoom time (10,000 nodes, culled) 5.0 ms 3.2 ms (React Flow is faster)
Drag time (10,000 nodes, culled) 4.3 ms 4.5 ms

Note: The bundle-size measurements describe a real application integration, while the interaction measurements come from synthetic benchmark graphs. They represent different workloads and should be interpreted separately.

Where Flowinger Wins: Bundle Size and Unculled Mounting

The QueryLens integration provides the most concrete demonstration of the bundle-size improvement. The diagram chunk decreased from 60.5 kB to 19 kB gzipped—a reduction of approximately 68.6%.

The mount benchmark also showed a substantial difference: with 10,000 nodes and viewport culling disabled, Flowinger mounted the graph in 0.7 seconds, compared with 1.3 seconds for React Flow (approximately 1.9× faster in this benchmark configuration).

These results are encouraging for applications that need to render large graphs without necessarily requiring the entire editing experience. The unculled interaction measurements also favored Flowinger in this test configuration.

However, benchmark results depend on the graph structure, rendering workload, browser, hardware, build configuration, and measurement methodology. They should not be interpreted as a guarantee that Flowinger will outperform React Flow in every application.

Where React Flow Wins: Viewport Culling

The comparison changes when viewport culling is enabled. In our reported 10,000-node benchmark, React Flow was faster at panning and zooming:

Interaction Flowinger React Flow
Pan 4.4 ms 3.1 ms
Zoom 5.0 ms 3.2 ms

Viewport culling avoids rendering nodes that are outside the visible area, reducing the amount of work required for large graphs.

Flowinger currently renders a safety margin beyond the visible viewport so nodes don’t immediately pop into view as the user pans. Its current visibility calculations also scan the node set to determine which nodes should be rendered. React Flow’s internal visibility calculations perform better in our measured scenario.

This is an area we intend to improve. Our roadmap includes investigating spatial indexing and other approaches to reduce the work required to identify visible nodes.

The takeaway isn’t that one implementation is universally faster. It’s that Flowinger currently shows advantages in some workloads, while React Flow remains faster in others. We want our performance claims to reflect that distinction.


4. Advanced Graph-Editing Utilities Without a Pro Subscription

Rendering nodes and edges is only one part of building a graph editor. Depending on the application, developers may also need:

  • Undo and redo
  • Clipboard operations
  • Alignment and snapping guides
  • SVG and PNG export
  • Smart edge routing
  • Editable edge interactions

These capabilities can require significant application-level integration, even when the underlying graph library already provides the basic rendering and interaction primitives.

Flowinger provides a collection of MIT-licensed utilities that developers can integrate independently:

import { useUndoRedo } from 'flowinger/history';
import { useClipboard } from 'flowinger/clipboard';
import { useSnapLines, SnapLinesOverlay } from 'flowinger/snaplines';
import { exportCanvasToSvg, exportCanvasToPng } from 'flowinger/export';
import { SmartEdge } from 'flowinger/smart-edges';
import { EditableEdge } from 'flowinger/editable-edge';
Enter fullscreen mode Exit fullscreen mode

These utilities are intended to be independently importable, allowing developers to adopt individual capabilities without treating them as one inseparable feature bundle.

The relevant distinction is that these are separately available, open-source modules—not a claim that React Flow’s core library charges for equivalent fundamental functionality. React Flow has its own mature ecosystem, documentation, and advanced examples. Developers should compare the exact features, licensing, and implementation requirements relevant to their application.

Example: Adding Undo and Redo

Here’s an example of integrating Flowinger’s history utility into an editor. Because hooks like useUndoRedo interact with graph state, the component is wrapped in a provider:

import { ReactFlow, ReactFlowProvider } from 'flowinger';
import { useUndoRedo } from 'flowinger/history';

function FlowEditor() {
  // Headless undo/redo: automatically attaches Cmd+Z / Cmd+Shift+Z shortcuts by default!
  const { undo, redo, canUndo, canRedo, takeSnapshot } = useUndoRedo({ maxHistory: 50 });

  return (
    <ReactFlow
      nodesDraggable
      onNodeDragStart={() => takeSnapshot()}
      onConnect={(params) => {
        takeSnapshot();
        // Apply your connection logic here.
      }}
    >
      <div className="toolbar">
        <button onClick={undo} disabled={!canUndo}>
          Undo (Cmd+Z)
        </button>
        <button onClick={redo} disabled={!canRedo}>
          Redo (Cmd+Shift+Z)
        </button>
      </div>
    </ReactFlow>
  );
}

export function App() {
  return (
    <ReactFlowProvider>
      <FlowEditor />
    </ReactFlowProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

This illustrates the integration pattern rather than a complete production-ready history implementation. A production editor still needs to ensure snapshots are captured at the correct points, state changes are reversible, keyboard shortcuts are registered and cleaned up correctly, and undo/redo restores the intended graph state.

The broader goal is to make these capabilities accessible without forcing every application to build them from scratch.


5. Testing Compatibility Against React Flow

Building an alternative renderer introduces a difficult question: How do we know that familiar APIs produce the expected results?

Matching function signatures is only the beginning. Graph libraries contain many subtle behaviors involving geometry, event handling, interaction state, and viewport calculations.

We use React Flow as a behavioral reference for selected compatibility tests. Our test suites cover areas including:

  • Handle-coordinate calculations
  • Connection-anchor calculations
  • Zoom clamping
  • Event signatures
  • Node interaction behavior
  • Multi-node dragging
  • Box selection
  • Keyboard navigation

The project includes more than 1,000 test assertions across unit, integration, and browser tests.

For geometry and other numerical calculations, the tests account for precision requirements. For interaction behavior, they check that the tested operations produce the expected outcomes. We also run Playwright tests against packaged production builds, helping catch integration problems that may not appear when running the source tree directly.

This approach doesn’t prove complete compatibility with every possible React Flow application, but it does give us a repeatable way to detect regressions in the behaviors we explicitly test.


6. When Should You NOT Switch?

Flowinger isn’t the right choice for every application, and we don’t want to pretend otherwise. You should consider staying with React Flow if any of the following apply:

  1. Your Application Depends Heavily on React Flow’s Default Styles: If your application extensively overrides .react-flow__handle, relies on --xy-* CSS variables, or assumes the exact behavior of React Flow’s default stylesheet, migrating may require additional styling work.
  2. You Need Every Interaction to Be Enabled by Default: Flowinger uses a viewer-first approach, rather than assuming every graph needs full editor interactions. For example, explicitly enable the interactions your application requires:
   <ReactFlow nodesDraggable nodesConnectable elementsSelectable />
Enter fullscreen mode Exit fullscreen mode
  1. Your Application Depends on Unverified Edge Cases: If you rely on unusual combinations of props, highly customized edge behavior, specialized keyboard interactions, or subtle event-ordering guarantees, validate those behaviors before switching.
  2. You Need Established Commercial Support: React Flow has an established ecosystem and dedicated maintainers. Flowinger is a community-oriented project focused on lightweight rendering, modularity, and compatibility.

For production migrations, we recommend testing your actual graph implementation rather than relying solely on synthetic benchmarks or API comparisons.


7. Try Flowinger

Flowinger is published on npm under the MIT license:

npm install flowinger
# or
pnpm add flowinger
Enter fullscreen mode Exit fullscreen mode

Explore the Examples

You can try the examples directly in your browser without setting up a local project:

Explore the Source

You can inspect the implementation, review the benchmark methodology, and follow development on GitHub:
👉 Flowinger on GitHub

If you’re considering migration, start with a small graph or read-only visualization. Compare the resulting production bundle, validate the interactions your application depends on, and report any compatibility gaps you encounter.


Final Thoughts

React Flow is an excellent library, and building Flowinger has given us a deeper appreciation for the engineering involved in a mature graph-rendering ecosystem.

Different applications have different requirements:

  • A workflow editor with complex interactions may benefit from a comprehensive framework.
  • A query-plan viewer or an embedded graph visualization may prioritize bundle size, predictable rendering, and a smaller dependency surface.

Flowinger is our attempt to make that second option easier without abandoning familiar React Flow API patterns.

If you were evaluating a replacement for React Flow, what would matter most to you: bundle size, rendering performance, API compatibility, or the availability of advanced editing utilities?

Try Flowinger on a real graph, let us know what works, and open an issue if you encounter a compatibility gap. We’re building it for developers who want a lighter graph-rendering option—and we’d love your feedback!

Top comments (0)