Skip to content

fix(desktop): optimize terminal rendering to prevent re-render loops#670

Closed
Kitenite wants to merge 1 commit intomainfrom
debug-slowness
Closed

fix(desktop): optimize terminal rendering to prevent re-render loops#670
Kitenite wants to merge 1 commit intomainfrom
debug-slowness

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Jan 8, 2026

Summary

  • Wrap Terminal and TabPane components in React.memo to prevent cascading re-renders from parent components
  • Use granular Zustand selectors in Terminal to subscribe only to specific pane properties (initialCommands, initialCwd, tabId) instead of the full pane object
  • Break circular dependency where Terminal updates pane.cwd → store notifies Terminal (which subscribed to pane) → Terminal re-renders
  • Use stable callback pattern for tRPC subscription to prevent subscription re-initialization on every render

Problem

When typing in the terminal, there were dropped frames and noticeable slowness. Investigation revealed:

  1. Terminal subscribed to the full pane object from Zustand store
  2. Terminal updates pane.cwd via updatePaneCwd when parsing OSC-7 sequences
  3. This triggered store notifications, causing Terminal to re-render
  4. The re-render created new callback references, potentially reinitializing the tRPC subscription
  5. This created a circular re-render loop causing poor frame rates

Test plan

  • Open the desktop app and navigate to a terminal
  • Type rapidly in the terminal
  • Verify smooth rendering without dropped frames
  • Verify cwd updates still work correctly (directory navigator shows current path)
  • Verify terminal focus/blur behavior works correctly

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Refactor
    • Enhanced rendering performance in tab and terminal components through strategic component memoization to reduce unnecessary re-renders and improve overall UI responsiveness.
    • Optimized terminal state management with granular data selectors and stabilized callback handling for better performance consistency.
    • Refined workspace tab pane visibility logic for more efficient tab content management.

✏️ Tip: You can customize this high-level summary in your review settings.

- Wrap Terminal and TabPane components in React.memo to prevent cascading re-renders
- Use granular Zustand selectors in Terminal to avoid subscribing to the full pane object
- Break circular dependency where Terminal updates cwd → store notifies → Terminal re-renders
- Use stable callback pattern for tRPC subscription to prevent re-initialization

This fixes dropped frames and slowness when typing in the terminal UI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

React.memo memoization is applied to TabPane and Terminal components for render optimization. The TabView pane derivation logic is refactored to filter panes directly from a global store instead of deriving them from layout structure, decoupling pane visibility from layout-based extraction.

Changes

Cohort / File(s) Summary
Component Memoization
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx
Wraps exported TabPane component with React.memo to prevent unnecessary re-renders. Function declaration converted to memoized const.
Pane Derivation Logic
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
Refactors tab-related panes derivation to filter from global panes store by tabId instead of extracting pane IDs from layout structure. Moves allPanes retrieval and replaces layout-driven logic with direct store-based filtering.
Terminal Component Refactor
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
Applies memoization to Terminal. Introduces granular state selectors for pane metadata (paneInitialCommands, paneInitialCwd, parentTabId). Stabilizes subscription callbacks using ref-backed handler indirection. Refactors focus tracking to use parentTabId and updates workspaceCwd/terminalLinkBehavior data fetching with separate queries.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A dash of memo, a sprinkle of memoize—
Render optimization's our latest prize!
Panes now flow from stores, not layouts old,
Terminal subscriptions kept steady and bold.
Performance improves with each careful tweak! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description is comprehensive and well-structured, covering the summary of changes, the problem being solved, and a test plan. However, it does not follow the required template structure with proper section headings like 'Related Issues', 'Type of Change', or 'Testing' as specified in the repository template. Reformat the description to match the required template structure, including sections for Related Issues, Type of Change, and Testing with appropriate checkboxes.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: optimizing terminal rendering to prevent re-render loops, which matches the core objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx (1)

47-55: Subscribing to all panes may still cause unnecessary re-renders.

The selector useTabsStore((s) => s.panes) subscribes to the entire panes object. Any pane update (e.g., cwd changes from any terminal) will produce a new allPanes reference and trigger a re-render of TabView, even if the update is for a pane in a different tab.

Consider using a more granular selector that only returns panes for this specific tab, or using Zustand's shallow equality check:

Option 1: Use shallow comparison
+import { shallow } from "zustand/shallow";
+
 // Get all panes belonging to this tab
-const allPanes = useTabsStore((s) => s.panes);
-const tabPanes = useMemo(
-  () =>
-    Object.fromEntries(
-      Object.entries(allPanes).filter(([_, pane]) => pane.tabId === tab.id),
-    ),
-  [allPanes, tab.id],
-);
+const tabPanes = useTabsStore(
+  (s) =>
+    Object.fromEntries(
+      Object.entries(s.panes).filter(([_, pane]) => pane.tabId === tab.id),
+    ),
+  shallow,
+);

This ensures TabView only re-renders when its own tab's panes actually change (by shallow-comparing the derived object's entries).

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0dd7368 and c209001.

📒 Files selected for processing (3)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
🧰 Additional context used
📓 Path-based instructions (6)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
apps/desktop/src/renderer/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

One component per file - do not create multi-component files

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
🧠 Learnings (1)
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/*/src/components/{ui,ai-elements,react-flow}/*.tsx : Use kebab-case single files for shadcn/ui components (e.g., button.tsx, base-node.tsx) in src/components/ui/, src/components/ai-elements, and src/components/react-flow/

Applied to files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build
🔇 Additional comments (4)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx (1)

44-58: Good use of memo() for render optimization.

The memoization combined with granular Zustand selectors (lines 60-61) should help reduce unnecessary re-renders when unrelated store state changes.

However, note that callback props like onMoveToTab and onMoveToNewTab are created inline in the parent's renderPane callback, which creates new references on each render and may partially defeat memo. Consider wrapping those callbacks with useCallback in the parent if you observe re-renders persisting.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx (3)

30-41: Excellent use of granular selectors to break the re-render loop.

The component now subscribes only to specific pane properties (initialCommands, initialCwd, tabId) that don't change after creation, rather than the entire pane object. This prevents the circular dependency where cwd updates would trigger re-renders.


57-59: Good defensive handling for potentially undefined parentTabId.

The fallback to empty string ensures no runtime error, and the conditional check on line 291 prevents calling setFocusedPane with undefined. This handles edge cases where the pane might not exist in the store during mount/unmount transitions.


246-286: Core fix: Stable callback pattern prevents subscription re-initialization.

This pattern correctly solves the re-render loop:

  1. stableOnData has a stable identity (empty deps useCallback)
  2. handleStreamDataRef.current is updated each render to capture fresh closures
  3. The subscription sees the same callback reference, preventing re-initialization

This breaks the cycle: Terminal updates cwd → store notifies → Terminal re-renders → but subscription callback identity unchanged → no subscription re-init.

@Kitenite Kitenite closed this Jan 10, 2026
@Kitenite Kitenite deleted the debug-slowness branch January 13, 2026 17:50
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.

1 participant