fix(apollo-react): eliminate unnecessary re-renders in StageNode canvas components#807
Merged
Conversation
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
Dependency License Review
License distribution
Excluded packages
|
There was a problem hiding this comment.
Pull request overview
Performance-focused refactor of apollo-react canvas StageNode subtree to reduce render churn during zoom/drag/selection interactions by stabilizing props/callbacks and gating store subscriptions to only when needed.
Changes:
- Refactors task context menu plumbing so
TaskMenubuilds items from a single task-keyed builder ((task) => items), avoiding per-task inline closures that defeatmemo(). - Reduces unnecessary XYFlow/Zustand subscriptions (e.g., zoom gating in
StageTaskDragOverlay, open-gateduseInternalNodesubscription inuseFloatingPosition) and mounts floating panels only while open. - Avoids repeated expensive DOM geometry work in
StageEdgeby memoizing arrow measurements by path.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/apollo-react/src/canvas/components/StageNode/TaskMenu.tsx | Updates TaskMenu API to accept task and task-keyed item builder. |
| packages/apollo-react/src/canvas/components/StageNode/StageTaskDragOverlay.tsx | Gates zoom selector so idle overlay doesn’t re-render on zoom changes. |
| packages/apollo-react/src/canvas/components/StageNode/StageNodeSequentialTaskGroups.tsx | Shares a stable (task) => items builder across tasks; removes per-task closures. |
| packages/apollo-react/src/canvas/components/StageNode/StageNodeEventDrivenTaskGroups.tsx | Passes stable task-keyed context menu builder to items. |
| packages/apollo-react/src/canvas/components/StageNode/StageNodeAdhocTaskGroups.tsx | Passes stable task-keyed context menu builder to items. |
| packages/apollo-react/src/canvas/components/StageNode/StageNode.tsx | Mounts FloatingCanvasPanel only while open; stabilizes close handlers; uses className="relative". |
| packages/apollo-react/src/canvas/components/StageNode/StageNode.test.tsx | Updates mocks to align with FloatingCanvasPanel defaulting open + new context-menu contract. |
| packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.utils.tsx | Hoists menu items and uses position-ignoring comparator to reduce story wrapper renders. |
| packages/apollo-react/src/canvas/components/StageNode/StageEdge.tsx | Memoizes arrow geometry calculation by pathData/arrowSize. |
| packages/apollo-react/src/canvas/components/StageNode/EventDrivenTask.tsx | Updates item prop types and TaskMenu usage to pass task. |
| packages/apollo-react/src/canvas/components/StageNode/AdhocTask.tsx | Updates item prop types and TaskMenu usage to pass task. |
| packages/apollo-react/src/canvas/components/StageNode/DraggableTask.types.ts | Removes group/task indices from props; switches menu builder to task-keyed signature. |
| packages/apollo-react/src/canvas/components/StageNode/DraggableTask.tsx | Switches from reactive zoom subscription to store API reads for drag scaling. |
| packages/apollo-react/src/canvas/components/StageNode/DraggableTask.test.tsx | Updates tests/mocks for useStoreApi and new menu builder signature. |
| packages/apollo-react/src/canvas/components/FloatingCanvasPanel/useFloatingPosition.ts | Subscribes to useInternalNode only while open to avoid frame-rate re-renders. |
kittyyueli
approved these changes
Jun 11, 2026
f05ef3e to
27dcf17
Compare
…as components Fixes the re-render storms visible in react-scan when interacting with stage nodes: - DraggableTask: gate the zoom store subscription on an active drag transform — the selector resolves a constant while idle, so canvas zoom changes no longer re-render every task, while an active drag still tracks zoom reactively - StageTaskDragOverlay: same drag-gated zoom selector so idle overlays stay quiet - StageNode: mount the add/replace-task FloatingCanvasPanels only while open, with stable onClose handlers (closed panels subscribed to node internals and re-rendered every drag frame) - useFloatingPosition: subscribe to useInternalNode only while open, fixing the same leak for all FloatingCanvasPanel consumers - Task context menus: unify on a single task-keyed contract (getContextMenuItems(task)) shared as one stable reference across all task items — per-task inline closures were defeating the memo on every DraggableTask/EventDrivenTaskItem/AdhocTaskItem, re-rendering all tasks on every parent render (worst: per pointermove during task drag) - StageEdge: memoize the detached-SVG arrow measurement (getTotalLength/getPointAtLength) by path string - StageNodeWrapper (stories): use areNodePropsEqualIgnoringPosition and hoist menuItems so node drags don't re-render the whole stage per frame Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
27dcf17 to
9833f29
Compare
CalinaCristian
approved these changes
Jun 11, 2026
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.
Summary
Comprehensive re-render audit + fixes for everything under
canvas/components/StageNode/. With react-scan, stage interactions previously showed continuous flashing and FPS drops; after these changes the tree only re-renders what actually changed.draggingflip)Changes
DraggableTask: drop the per-taskuseStore(s => s.transform[2])subscription; zoom is read non-reactively viauseStoreApiinside the drag style memo (it's only needed while a drag transform exists, and dnd-kit recomputes per frame then anyway).StageTaskDragOverlay: zoom selector gated on an active drag — idle overlays return a constant.StageNode: add/replace-taskFloatingCanvasPanels are mounted only while open, with stableonClosehandlers; root div usesclassName="relative"per styling standards.useFloatingPosition(shared): subscribe touseInternalNodeonly whileopen— closed-but-mounted panels re-rendered on every drag/measure frame of their anchor node. Fixes allFloatingCanvasPanelconsumers, not just StageNode.getContextMenuItems?: (task: StageTaskItem) => NodeMenuItem[]— passed as a single stable reference to every task item. Per-task inline closures were defeatingmemo()onDraggableTask/EventDrivenTaskItem/AdhocTaskItem.TaskMenunow takestaskand calls the builder itself, removing three duplicated wrapper callbacks andDraggableTask's now-deadgroupIndex/taskIndexprops. Items are still built lazily, only when a menu opens.StageEdge: the detached-SVG arrow measurement (getTotalLength/getPointAtLength) is memoized by path string, so selection toggles skip the DOM geometry work.StageNodeWrapper(stories): usesareNodePropsEqualIgnoringPositionand hoistsmenuItems— XYFlow's per-framepositionAbsoluteX/Yupdates plus an inline array were re-rendering the entire node body at drag rate.Before
Kapture.2026-06-11.at.12.06.17.mp4
After
Kapture.2026-06-11.at.12.03.54.mp4
Validation
vitest run src/canvas— 1449/1449 passtsc --noEmit— no errors insrc/canvas/**(pre-existingmaterial/themeerrors unrelated)biome check— no new warningsNot included (follow-ups)
StageHandle.tsx(unused; subscribes touseConnection()per pointermove if ever wired up)propspass-down to header/group components (fold into the Tailwind migration)TaskPlayButtondebounce.clear()on unmount🤖 Generated with Claude Code