Skip to Content

Performance

When you’re dealing with a large number of nodes or complex custom components, performance can become challenging. Here are a few effective strategies to keep Vue Flow fast.

Keep reactivity shallow

The most common source of performance problems is unnecessary reactivity. Node drags, pans, and zooms all update state frequently, so the cost of each update matters.

Store your nodes and edges in a shallowRef rather than a deep ref, and replace the whole array when you change it (for example with Array.prototype.map) instead of mutating individual nodes. A deep ref makes every property of every node reactive, which is expensive for large flows and buys you nothing here, because Vue Flow tracks changes at the array level.

import type { Node } from '@xyflow/vue'; import { shallowRef } from 'vue'; // one dependency for the whole array const nodes = shallowRef<Node[]>([/* ... */]); // update by replacing, not mutating nodes.value = nodes.value.map(n => (n.id === '1' ? { ...n, selected: true } : n));

With v-model:nodes / v-model:edges, Vue Flow already replaces the whole array when it applies a change, so a shallowRef is all you need. See Adding Interactivity.

Memoize expensive custom nodes

Custom nodes and edges are ordinary Vue components, so Vue only re-renders them when their props change. If a custom node has an expensive template that depends on only a few props, you can skip re-rendering while those props are unchanged with v-memo:

<script setup lang="ts"> import type { NodeProps } from '@xyflow/vue'; const props = defineProps<NodeProps>(); </script> <template> <div v-memo="[props.data.label, props.selected]" class="expensive-node"> <!-- only re-renders when label or selected changes --> </div> </template>

Unlike React, you don’t need to wrap handlers or option objects such as defaultEdgeOptions and snapGrid in anything: <script setup> runs once, so a function or object declared there keeps a stable reference across renders.

Only render what’s visible

For very large graphs, the biggest win is to not render off-screen elements at all. Set onlyRenderVisibleElements and Vue Flow renders only the nodes and edges inside the current viewport, mounting and unmounting them as you pan and zoom:

<template> <VueFlow v-model:nodes="nodes" v-model:edges="edges" :only-render-visible-elements="true" /> </template>

This trades a little work on each viewport change for a much smaller DOM, and it’s the single most effective option once you have thousands of nodes.

Don’t read the whole nodes array in a component

A common pitfall is pulling the entire nodes or edges array into a component that needs only a slice of it. Those arrays change on every drag, pan, and selection, so any component that depends on them re-renders constantly.

Reacts to every change:
<script setup lang="ts"> import { storeToRefs, useVueFlowStore } from '@xyflow/vue'; import { computed } from 'vue'; // reacts to every node change, even unrelated ones const { nodes } = storeToRefs(useVueFlowStore()); const selectedIds = computed(() => nodes.value.filter(n => n.selected).map(n => n.id)); </script>

Instead, read only what you need through a targeted composable such as useNodesData or useNodeConnections, so the component reacts only when that specific data changes:

<script setup lang="ts"> import { useNodesData } from '@xyflow/vue'; // only reacts when these nodes' data changes const data = useNodesData(['1', '2']); </script>

Collapse large node trees

If your graph is deeply nested, rendering every node at once is wasteful. Render a limited set and let users expand on demand by toggling each node’s hidden field with updateNode:

import { useVueFlow } from '@xyflow/vue'; const { updateNode } = useVueFlow(); function toggleChildren(node) { for (const childId of node.data.children ?? []) { updateNode(childId, prev => ({ hidden: !prev.hidden })); } }

Hiding nodes until they’re expanded keeps the rendered graph small without losing any state.

Simplify node and edge styles

If you’ve optimized everything else and large graphs still feel sluggish, look at your CSS. Complex styles, especially animations, gradients, and box-shadows, are re-rasterized by the browser on every frame while panning and zooming, which can dominate the frame budget.

A heavy, multi-layer blurred box-shadow on nodes is a frequent culprit: it looks fine when static but hurts pan and zoom smoothness on large flows in Chromium. Prefer a light, single-layer shadow or a border for nodes you render in bulk.

See it at scale

The stress test example renders a large graph you can pan and zoom to feel these trade-offs in practice.

Last updated on