Fix canvas render lag: LOD fan-out, unmemoized markdown, prop churn - #319
Merged
Conversation
…own, prop churn useLodVisibility subscribed to the raw zoom number instead of the derived LOD boolean, so every node view re-rendered on every animation frame of any zoom/pan gesture. Verified live: ~15ms of main-thread work per wheel step on a 63-node scene, most of it this fan-out re-rendering NodeMarkdown's full remark/rehype/KaTeX/highlight parse pipeline, which was itself unmemoized. Also stabilizes SceneCanvas's static <ReactFlow> props and the onSelectionChange/onEdgeMouseEnter/onEdgeMouseLeave callbacks so they don't mint fresh references every render, makes the drag-frame smart-guide state update bail out when nothing actually changed, and suppresses the node-card hover transition while dragging (the controlled-drag round-trip lag was toggling :hover on/off between frames, reading as flicker). Re-measured on the same 63-node scene after the fix: zoom cost dropped to ~1.4ms median (max 2.4ms) per wheel step, roughly a 10x reduction. ChatNodeView.test.tsx's NodeMarkdown mock is updated to match: memo() wraps NodeMarkdown in an object that isn't directly callable, so the vi.fn() spy now wraps a plain function that renders the real memoized component via createElement, preserving both the original call-count assertions and genuine markdown rendering in tests. Co-Authored-By: Claude Sonnet 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
The canvas UI was reported as glitchy/laggy, with nodes visibly fidgeting behind the cursor while dragging. Live measurement on a 63-node scene traced the dominant cost to
useLodVisibility: it subscribed to the raw zoom number from React Flow's store rather than the derived LOD boolean, so every node view (~14 kinds) re-rendered on every animation frame of any zoom/pan gesture - not just when the LOD threshold actually crossed. That fan-out repeatedly re-ranNodeMarkdown's full remark/rehype/KaTeX/highlight.js parse pipeline, which was itself unmemoized. Measured cost: ~15ms of main-thread work per wheel step (near the full 16.6ms 60fps budget) on the same scene.Secondary contributors:
SceneCanvas's static<ReactFlow>props (deleteKeyCode,proOptions,defaultEdgeOptions,snapGrid) and event-handler props (onSelectionChange,onEdgeMouseEnter,onEdgeMouseLeave) were fresh literals/closures on every render; the drag-frame smart-guide state update fired unconditionally even when nothing changed; and the controlled-drag round-trip lag let the cursor drift outside a dragged node's true rect between frames, toggling:hoveron/off and replaying its border/box-shadow transition as visible flicker.Change
useLodVisibility.ts- selector now returns the boolean directly (s.transform[2] < LOD_ZOOM_THRESHOLD) so subscribers only re-render on an actual LOD flip.NodeMarkdown.tsx- memoized the component; hoisted the static plugin/component config to module scope.SceneCanvas.tsx- hoisted static<ReactFlow>props to stable references, wrapped selection/edge-hover handlers inuseCallback, made the smart-guide state update bail out viaObject.iswhen unchanged.styles.css- pins node-card hover chrome to its resting state while.react-flow__node.dragging, so the round-trip lag can't trigger transition flicker.ChatNodeView.test.tsx-NodeMarkdownis nowmemo(...), an object rather than a directly-callable function, which broke the existingvi.fn(actual.NodeMarkdown)mock (.apply is not a function). Fixed by spying on a plain wrapper that renders the real memoized component viacreateElement, preserving both the original call-count assertions and genuine markdown rendering in tests.Test plan
npm run checkpasses end-to-end (schema, typecheck, lint - 0 errors, 1922/1922 tests, build, bundle-size)