Skip to content

fix(apollo-react): eliminate unnecessary re-renders in StageNode canvas components#807

Merged
snuziale merged 1 commit into
mainfrom
perf/stage-node-rerenders
Jun 11, 2026
Merged

fix(apollo-react): eliminate unnecessary re-renders in StageNode canvas components#807
snuziale merged 1 commit into
mainfrom
perf/stage-node-rerenders

Conversation

@snuziale

@snuziale snuziale commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

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.

Interaction Before After
Zoom / pinch every task in every stage re-rendered per frame nothing re-renders
Dragging a stage node floating panels + whole node body re-rendered per frame one render at drag start/end (dragging flip)
Dragging a task all tasks in the stage re-rendered per pointermove only the placeholder + drag overlay
Selecting a stage all task rows re-rendered header/group shells once; task rows quiet

Changes

  • DraggableTask: drop the per-task useStore(s => s.transform[2]) subscription; zoom is read non-reactively via useStoreApi inside 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-task FloatingCanvasPanels are mounted only while open, with stable onClose handlers; root div uses className="relative" per styling standards.
  • useFloatingPosition (shared): subscribe to useInternalNode only while open — closed-but-mounted panels re-rendered on every drag/measure frame of their anchor node. Fixes all FloatingCanvasPanel consumers, not just StageNode.
  • Task context menus: unified on one task-keyed contract — getContextMenuItems?: (task: StageTaskItem) => NodeMenuItem[] — passed as a single stable reference to every task item. Per-task inline closures were defeating memo() on DraggableTask/EventDrivenTaskItem/AdhocTaskItem. TaskMenu now takes task and calls the builder itself, removing three duplicated wrapper callbacks and DraggableTask's now-dead groupIndex/taskIndex props. 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): uses areNodePropsEqualIgnoringPosition and hoists menuItems — XYFlow's per-frame positionAbsoluteX/Y updates 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 pass
  • tsc --noEmit — no errors in src/canvas/** (pre-existing material/theme errors unrelated)
  • biome check — no new warnings
  • Manual react-scan checklist: zoom, stage drag, task drag, selection (table above)

Not included (follow-ups)

  • Delete dead StageHandle.tsx (unused; subscribes to useConnection() per pointermove if ever wired up)
  • Granular props instead of whole-props pass-down to header/group components (fold into the Tailwind migration)
  • TaskPlayButton debounce .clear() on unmount

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings June 11, 2026 19:04
@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (PT)
apollo-design 🟢 Ready Preview, Logs Jun 11, 2026, 04:13:21 PM
apollo-docs 🟢 Ready Preview, Logs Jun 11, 2026, 04:13:21 PM
apollo-landing 🟢 Ready Preview, Logs Jun 11, 2026, 04:13:21 PM
apollo-vertex 🟢 Ready Preview, Logs Jun 11, 2026, 04:13:21 PM

@github-actions

Copy link
Copy Markdown
Contributor

Dependency License Review

  • 1922 package(s) scanned
  • ✅ No license issues found
  • ⚠️ 2 package(s) excluded (see details below)
License distribution
License Packages
MIT 1692
ISC 89
Apache-2.0 55
BSD-3-Clause 27
BSD-2-Clause 23
BlueOak-1.0.0 8
MPL-2.0 4
MIT-0 3
CC0-1.0 3
MIT OR Apache-2.0 2
(MIT OR Apache-2.0) 2
Unlicense 2
LGPL-3.0-or-later 1
Python-2.0 1
CC-BY-4.0 1
(MPL-2.0 OR Apache-2.0) 1
Unknown 1
Artistic-2.0 1
(WTFPL OR MIT) 1
(BSD-2-Clause OR MIT OR Apache-2.0) 1
CC-BY-3.0 1
0BSD 1
(MIT OR CC0-1.0) 1
MIT AND ISC 1
Excluded packages
Package Version License Reason
@img/sharp-libvips-linux-x64 1.2.4 LGPL-3.0-or-later LGPL pre-built binary, not linked
khroma 2.1.0 Unknown MIT per GitHub repo, missing license field in package.json

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TaskMenu builds items from a single task-keyed builder ((task) => items), avoiding per-task inline closures that defeat memo().
  • Reduces unnecessary XYFlow/Zustand subscriptions (e.g., zoom gating in StageTaskDragOverlay, open-gated useInternalNode subscription in useFloatingPosition) and mounts floating panels only while open.
  • Avoids repeated expensive DOM geometry work in StageEdge by 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.

Comment thread packages/apollo-react/src/canvas/components/StageNode/DraggableTask.tsx Outdated
@snuziale snuziale force-pushed the perf/stage-node-rerenders branch 2 times, most recently from f05ef3e to 27dcf17 Compare June 11, 2026 23:06
@snuziale snuziale changed the title perf(apollo-react): eliminate unnecessary re-renders in StageNode canvas components fix(apollo-react): eliminate unnecessary re-renders in StageNode canvas components Jun 11, 2026
…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>
@snuziale snuziale force-pushed the perf/stage-node-rerenders branch from 27dcf17 to 9833f29 Compare June 11, 2026 23:07
@snuziale snuziale enabled auto-merge (rebase) June 11, 2026 23:21
@snuziale snuziale merged commit bdd13d3 into main Jun 11, 2026
42 checks passed
@snuziale snuziale deleted the perf/stage-node-rerenders branch June 11, 2026 23:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg:apollo-react size:L 100-499 changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants