Skip to content

Stop node components re-rendering on every frame of a drag - #462

Open
Vein05 wants to merge 1 commit into
ianarawjo:mainfrom
Vein05:perf/node-drag-rerender
Open

Vein05 wants to merge 1 commit into
ianarawjo:mainfrom
Vein05:perf/node-drag-rerender

Conversation

@Vein05

@Vein05 Vein05 commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Issue

React Flow passes each node component its position as props, so the node
being dragged re-renders its whole Mantine subtree on every pointer move.
None of the node components read the position -- they take only
{ data, id }, plus type for CodeEvaluatorNode, which serves two node types.

Not the same as #458: that was a node subscribing to the whole nodes array,
so nodes standing still re-rendered. This is the dragged node re-rendering
itself, and it costs the same at 3 nodes as at 48.

Fix

A helper, src/memoNode.tsx, wraps a node component in React.memo comparing
only the props these components read:

(a, b) => a.id === b.id && a.data === b.data && a.type === b.type

and each of the 21 node components exports through it:

export default memoNode(PromptNode);

Comparing data by identity works because setDataPropsForNode replaces it
with a copy (store.tsx:1100). The helper's docstring notes the constraint: a
node component that starts reading selected or dragging has to compare
them here too.

Numbers

Two production builds served side by side and profiled in one browser
session (Chrome CPU profile over CDP), dragging a node for ~2.5s, two runs
each, M-series Mac. Share of wall clock off-idle, and total off-idle CPU:

                   before            after
prompt node        47%  (1374 ms)    23%  (546 ms)
table node         46%  (1079 ms)    22%  (491 ms)
inspect node       26%  ( 561 ms)    21%  (461 ms)

The prompt node went from ~23 ms of CPU per frame to ~9 ms, so a drag that
could not hold 60fps now fits in the frame budget. Each node type pays this
cost when it is the one being dragged, which is why the table node gains as
much as the prompt node.

Checked

prettier -c, eslint, tsc --noEmit and craco test (68 suites, 1234 tests) pass.

React Flow passes each node component its position as props, so dragging a
node re-renders that node -- and its whole Mantine subtree -- on every pointer
move. Node components here read only `data`, `id` and, for the evaluator that
serves two node types, `type`, so a position change cannot change what they
render.

Wrap them in React.memo through a shared `memoNode` helper, comparing those
props alone. `data` compares by identity because the store never mutates it in
place under its old identity: `setDataPropsForNode` replaces it with a copy.

Measured on a production build, dragging one node for ~2.5s (Chrome CPU
profile, share of wall clock spent off-idle):

    prompt node    47% -> 23%   (1374 ms -> 546 ms)
    table node     46% -> 22%   (1079 ms -> 491 ms)
    inspect node   26% -> 21%   ( 561 ms -> 461 ms)

At ~23 ms of CPU per frame the prompt node could not hold 60fps while being
dragged; at ~9 ms it can.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant