Skip to content

Fix nodes and connections blinking after every scene update - #320

Merged
dovvnloading merged 1 commit into
mainfrom
fix/edge-blink-on-scene-update
Aug 14, 2026
Merged

Fix nodes and connections blinking after every scene update#320
dovvnloading merged 1 commit into
mainfrom
fix/edge-blink-on-scene-update

Conversation

@dovvnloading

Copy link
Copy Markdown
Owner

Problem

While moving nodes around the canvas, their connection lines were visibly failing to keep up: they would drop out for a moment and then reappear, and the node card itself could briefly vanish. The effect was most noticeable on whichever node the user had just selected or dragged, and it also showed up during streamed responses and shortly after panning or zooming.

The cause is in how the canvas rebuilds its node list whenever the backend sends a scene update. React Flow, the library that renders the canvas, records each node's on-screen size on the node object itself once it has measured it. Our rebuild produced fresh node objects that did not carry that recorded size forward. When React Flow received a node without its size, it treated the node as never measured: it hid the node until it could measure it again, and it removed every connection attached to it, because a connection cannot be drawn to an unmeasured node. The re-measurement takes at least one frame, so each scene update produced a visible blink. Scene updates happen constantly in normal use - after every drag is released, after every pan or zoom is reported to the backend, and on every streamed chunk of a response - so the blinking read as the canvas constantly stuttering.

An existing step in the rebuild made this worse. The function that preserves the user's selection across updates created a fresh copy of every selected node on every update, which meant the selected node lost its recorded size even when nothing about it had actually changed.

Change

The selection-preserving step (withPreservedSelection, now renamed withPreservedFlowState) carries the recorded size across the rebuild along with the selection flag. With the size present, React Flow keeps the node's existing geometry: the node stays visible and its connections stay drawn while any re-measurement happens in the background. Nodes that come out of the rebuild as the exact same object as before pass through untouched, keeping the existing fast path for unchanged nodes.

Each step of the failure was confirmed by reading the installed React Flow source rather than assumed: when a node object is replaced, the library rebuilds its internal record for that node; it keeps the connection-point geometry only if the incoming object carries a recorded size; a connection to an unmeasured node is not drawn at all; and a node without dimensions is rendered invisible.

Test plan

  • Full frontend check passes end to end: schema check, type check, lint (0 errors), 1927/1927 tests, production build, bundle-size check
  • Five new unit tests cover the size carry-over: it is applied to rebuilt nodes, combined correctly with selection, never invented when no size was recorded, never overrides a size the rebuilt node already has, and never breaks the untouched fast path for unchanged nodes
  • All existing selection-preservation tests pass unchanged against the renamed function

Every scene update from the backend rebuilds the canvas's node objects.
React Flow stores each node's measured size on those objects, and when a
rebuilt object arrives without that measurement, React Flow discards the
node's size and connection-point geometry and has to measure the node
again from scratch. Until that re-measurement lands - at least one frame
later - the node is rendered invisible and every connection touching it
is removed outright. Because updates arrive after every drag, every
pan or zoom report, and every streamed response chunk, the practical
effect was nodes and their connections flickering during ordinary use,
worst on whichever node the user had just selected or moved.

The selection-preserving step that already runs on every rebuild
(withPreservedSelection) made this worse: it cloned every selected
node into a fresh object on every update, so the selected node lost its
measurement even when nothing about it had changed.

Fix: that step is now withPreservedFlowState, and it carries the
measured size across the rebuild alongside the selection flag. With the
measurement present, React Flow keeps the node's existing geometry, so
nothing turns invisible and no connection is removed while a node waits
to be measured again. Nodes that kept their exact object identity pass
through untouched, so the fast path for unchanged nodes is preserved.

Verified against the installed @xyflow/system source at each step:
adoptUserNodes only rebuilds a node's internals when the object
reference changes, keeps handle geometry only when the incoming object
carries a measurement, getEdgePosition returns null for an unmeasured
node (unmounting the edge), and the node wrapper renders
visibility:hidden until dimensions exist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dovvnloading
dovvnloading merged commit 0adebdb into main Aug 14, 2026
4 checks passed
@dovvnloading
dovvnloading deleted the fix/edge-blink-on-scene-update branch August 14, 2026 14:55
dovvnloading added a commit that referenced this pull request Aug 14, 2026
These three changes were attempts at the drag artifact that were not
confirmed to work before shipping, and the most recent one made things
worse: with #324 in place the connection lag became visible in the light
theme as well, where it had previously been unnoticeable.

The most likely cause of that regression is the composition flag. The
audit behind #324 established that the flag added in #323 never reached
the browser, because the window library passes a conflicting switch and
duplicate switches resolve last-one-wins. #324 corrected the delivery,
so disabling delegated composition took effect for the first time - and
it applies to both themes equally, which matches the report exactly. The
stroke width increase and the explicit high-quality smoothing hint in
the same change also affect both themes and cannot be ruled out.

This restores graphlink_desktop.py, styles.css and gl-vars-dev.css to
their state as of #321. The measured performance fixes in #319, #320 and
#321 are untouched.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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