Skip to content

feat(workflow): resolve canvas re-rendering and logs perf issues#3844

Open
adithyaakrishna wants to merge 6 commits intosimstudioai:stagingfrom
adithyaakrishna:feat/workflow
Open

feat(workflow): resolve canvas re-rendering and logs perf issues#3844
adithyaakrishna wants to merge 6 commits intosimstudioai:stagingfrom
adithyaakrishna:feat/workflow

Conversation

@adithyaakrishna
Copy link
Copy Markdown
Contributor

Summary

Fixes severe UI lag and re-rendering issues on the workflow page during parallel loop execution. The canvas, output panel, input tab, and terminal logs all suffered from cascading re-renders when many iterations ran simultaneously.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Other: Perf optimization + code reorg

Testing

  • Run a workflow with a Parallel containing a Loop of 100+ iterations
  • Verify the canvas remains responsive during execution
  • Verify the Output panel does not flicker between iteration results
  • Verify terminal logs auto-scroll to the bottom as new iterations complete
  • Verify scrolling up in logs stays in place while new entries arrive
  • Verify fast scrolling in logs does not show white flash
  • Verify sandbox mode still works (isSandbox prop propagation)

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@cursor
Copy link
Copy Markdown

cursor bot commented Mar 30, 2026

PR Summary

Medium Risk
Moderate risk due to substantial refactors in workflow canvas interaction logic and terminal virtualization/scroll behavior, which could introduce subtle UX regressions despite being largely performance-focused.

Overview
Addresses workflow-page performance issues by reducing cascading React re-renders across the terminal logs, output panel, and workflow canvas during high-volume executions (e.g., nested parallel/loop runs).

Terminal/output changes: StructuredOutput now avoids resetting expanded state when data identity changes but JSON content is unchanged (prevents flicker), the output header toolbar is extracted into a memoized component, terminal log virtualization is reworked to use an external “signal store” (useSyncExternalStore) plus higher overscan, stable row data refs, and auto-scroll only when near bottom.

Workflow canvas changes: introduces new hooks (useBlockOperations, useCanvasKeyboard, useAutoConnectEdge, useNodeDerivation, useLockNotifications) and shared constants (workflow-constants.ts) to centralize canvas behaviors (drop/add/paste/duplicate, auto-connect edges, selection syncing, lock notifications) while trimming block props/state plumbing (remove isActive/isPending props; derive pending/run status from execution store selectors).

Written by Cursor Bugbot for commit 0ec73b0. This will update automatically on new commits. Configure here.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 30, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 30, 2026 3:46pm

Request Review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 30, 2026

Greptile Summary

This PR fixes severe UI lag during parallel/loop workflow execution by decoupling execution state from canvas node derivation and virtualizing the terminal log list. The approach is architecturally sound and the core optimizations are well-targeted.

Key changes:

  • Canvas re-render fix: isActive/isPending are removed from derivedNodes node data. Each WorkflowBlock now subscribes independently to its own execution state via useIsBlockActive / useIsBlockPending fine-grained selectors, so active-block updates no longer force a full re-derivation of every canvas node.
  • shouldSkipBlockRender custom memo comparator: Excludes xPos/yPos from comparison to prevent 100+ re-renders per drag, while preserving all semantically meaningful props.
  • Terminal virtualization: The log list is rewritten with react-window. A custom RowSignalStore + useSyncExternalStore pattern passes selection/expand state to virtual rows without prop drilling, so only the affected row re-renders.
  • Smart auto-scroll: Uses a proximity ref (isNearBottomRef) so auto-scroll to the latest entry only fires when the user is already near the bottom, preserving manual scroll position.
  • OutputPanel extraction: Converted to a standalone React.memo component that reads store settings directly, reducing prop drilling in the terminal header.

Issues found:

  • Two relative imports in use-block-state.ts violate the mandatory absolute-import rule.
  • shouldSkipBlockRender omits isSandbox and isEmbedded from comparison — semantically incomplete, though harmless while those props are static.
  • blocksStructureHash in derivedNodes's dep array alongside blocks is redundant; having blocks already covers the same invalidation signal.
  • Inline () => onToggleNode(iterNode.entry.id) closures in SubflowNodeRow prevent IterationNodeRow.memo from bailing out during hot parallel-loop renders.

Confidence Score: 5/5

Safe to merge — all remaining findings are P2 style/best-practice issues with no runtime impact.

The core optimization (decoupling execution state from canvas node derivation, virtualizing terminal logs) is correct and addresses a real performance problem. No P0/P1 bugs were found. The previous review concern about isActive/isPending still being computed in derivedNodes has been addressed in the fix commits. Remaining findings are a relative-import rule violation and minor memo-completeness/redundancy issues that do not affect runtime behavior.

use-block-state.ts (relative imports), workflow-block/utils.ts (shouldSkipBlockRender memo completeness), terminal.tsx (inline closure in SubflowNodeRow).

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-derivation.ts Core canvas optimization: removes execution state (active/pending) from node data derivation so parallel-loop execution no longer triggers full canvas recomputation. Introduces blocksStructureHash for coarse structural change detection. Minor: blocksStructureHash in derivedNodes deps alongside blocks is redundant.
apps/sim/stores/execution/store.ts Adds fine-grained useIsBlockActive and useIsBlockPending selectors so individual blocks subscribe directly to their execution state, avoiding broad re-renders across the canvas. Clean implementation.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-block-state.ts Refactored to use useIsBlockActive from the execution store directly. Contains two relative imports that violate the project's mandatory absolute-import rule.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/utils.ts Introduces shouldSkipBlockRender for React.memo comparison. Correctly skips position props to prevent drag re-renders, but omits isSandbox and isEmbedded from the comparison, making it semantically incomplete for those props.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx Large refactor: virtualizes the log list via react-window with a custom RowSignalStore + useSyncExternalStore pattern to avoid prop-drilling re-renders. Inline onToggle arrow inside SubflowNodeRow undermines IterationNodeRow.memo for nested children.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx Extracted into a standalone React.memo component; reads store-backed settings directly to reduce prop drilling. All callbacks are memoized correctly. Clean refactor.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts Adds flattenVisibleExecutionRows and VisibleTerminalRow types to support virtualized row rendering. Tree-building helpers for complex nested execution structures look correct.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-visual.ts Clean extraction of visual state logic from WorkflowBlock; uses fine-grained selectors so only the specific rendering block re-renders when its execution state changes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph Before["Before PR (cascade re-render)"]
        ES1[ExecutionStore setActiveBlocks] --> ND1[derivedNodes recomputes ALL nodes]
        ND1 --> DN1[displayNodes updates]
        DN1 --> RF1[ReactFlow re-renders all WorkflowBlocks]
    end
    subgraph After["After PR (isolated re-render)"]
        ES2[ExecutionStore setActiveBlocks] --> UIBA[useIsBlockActive per-block selector]
        UIBA --> BV[useBlockVisual only affected block]
        BV --> WB[Only that WorkflowBlock re-renders]
        WS[WorkflowStore block structure change] --> ND2[derivedNodes recomputes]
        ND2 --> DN2[displayNodes updates all nodes]
    end
    subgraph Terminal["Terminal Virtualization"]
        CE[ConsoleEntries] --> EG[groupEntriesByExecution]
        EG --> FVR[flattenVisibleExecutionRows]
        FVR --> VL[react-window List virtualized rows]
        RSS[RowSignalStore useSyncExternalStore] --> VER[VirtualEntryNodeRow re-renders on change]
        VL --> VER
    end
Loading

Reviews (3): Last reviewed commit: "chore: fix review changes" | Re-trigger Greptile

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 30, 2026

@adithyaakrishna is attempting to deploy a commit to the Sim Team on Vercel.

A member of the Team first needs to authorize it.

@adithyaakrishna
Copy link
Copy Markdown
Contributor Author

@greptile review

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

if (prevDataJsonRef.current !== newJson) {
prevDataJsonRef.current = newJson
setExpandedPaths(computeInitialPaths(data, isError))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unguarded JSON.stringify can throw on non-serializable data

Medium Severity

The new deep-equality check calls JSON.stringify(data) without a try-catch. If data contains circular references, BigInt values, or other non-JSON-serializable structures, this will throw an unhandled error and crash the structured output component. The previous code only compared by reference (prevDataRef.current !== data), which can never throw.

Additional Locations (1)
Fix in Cursor Fix in Web

dataRef.current = { rows }

const signalStoreRef = useRef(createRowSignalStore())
signalStoreRef.current.update(selectedEntryId, expandedNodes, onSelectEntry, onToggleNode)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Signal store mutated during React render phase

Low Severity

signalStoreRef.current.update() is called during the render of TerminalLogsPane, not inside a useEffect. When the update detects changes, it synchronously fires all useSyncExternalStore listeners. This is a side effect during render, violating React's requirement that rendering be pure. It can cause issues in React Strict Mode (double invocation) and is fragile in concurrent rendering scenarios.

Fix in Cursor Fix in Web

@waleedlatif1 waleedlatif1 deleted the branch simstudioai:staging April 3, 2026 23:01
@waleedlatif1 waleedlatif1 reopened this Apr 3, 2026
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.

2 participants