Fix nodes and connections blinking after every scene update - #320
Merged
Conversation
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>
2 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 renamedwithPreservedFlowState) 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