20 KiB
React Performance Audit
Status: in progress
Started: 2026-05-29
Base commit: b7fe967780 (origin/main, release v1.4.35)
Goal
Scan the full React surface for $perf and $react-useeffect issues, keep coverage evidence here, and land the fixes as many small PRs with explicit merge-risk notes.
This document is the audit ledger. A section is not considered fully scanned until it has:
- File inventory checked from the current worktree.
useEffect/useLayoutEffect/useInsertionEffectsites classified.- Perf-sensitive patterns checked: timers, subscriptions, observers,
JSON.stringify, storage writes, polling, broad store subscriptions, large list rendering, and render-time derived state. - Each suspicious site dispositioned as "no change", "needs PR", or "covered by PR".
- Merge risk recorded for every PR candidate.
Scope
React hook call scan uses all repo *.ts and *.tsx files, then narrows to actual AST hook calls. The React Effect surface found on 2026-05-29 is:
src/renderer/src/**mobile/app/**mobile/src/**mobile/packages/expo-two-way-audio/src/hooks.ts
Comment-only mentions outside those paths were ignored.
Initial inventory:
| Metric | Count |
|---|---|
Repo *.ts / *.tsx files |
3,011 |
| Files with real Effect hook calls | 285 |
| Effect hook call sites | 970 |
useLayoutEffect / useInsertionEffect call sites |
44 |
| Empty dependency arrays | 111 |
| Effects with cleanup returns | 472 |
| Effects with subscription/listener/observer shape | 170 |
| Effects with timer or animation-frame shape | 128 |
Effects with JSON.stringify shape |
2 |
| Set-state-shaped Effects needing manual review | 270 |
useMemo call sites in React scope |
633 |
useCallback call sites in React scope |
1,415 |
useSyncExternalStore call sites in React scope |
8 |
Coverage Ledger
Current count after low-risk PRs #3038, #3041, #3042, #3044, #3051, #3052, #3053, #3054, #3055, #3056, #3058, #3059, and #3060: 950 Effect hook call sites.
| Area | Files / signal | Scan status | Notes |
|---|---|---|---|
| Renderer app shell | src/renderer/src/App.tsx, root components |
Inventory complete, manual review pending | Check global listeners, beforeunload, media-query, sidebar resize, active-tab repair. |
| Terminal / PTY | components/Terminal.tsx, components/terminal-pane/**, components/terminal/** |
Inventory complete, manual review pending | High-risk area: xterm lifecycle, scrollback, remote/mobile parity, focus, WebGL, resize. |
| Browser pane | components/browser-pane/** |
Inventory complete, manual review in progress | Highest Effect density: 62 sites in BrowserPane.tsx. Check driver sync, address bar derived state, find state, webview lifetime. |
| Editor / markdown / Monaco | components/editor/** |
Inventory complete, manual review in progress | 117 area Effects. Check editor model cleanup, preview scroll restore, search debounce, generated decorations. |
| Sidebar / worktrees | components/sidebar/** |
Inventory complete, manual review in progress | Check worktree list state repair, drag/drop global listeners, kanban pointer flows, inline rename. |
| Right sidebar / source control | components/right-sidebar/** |
Inventory complete, manual review pending | Check polling, PR checks, file explorer watch/reveal, source-control local resets. Git provider compatibility required. |
| Settings | components/settings/** |
Inventory complete, manual review pending | 81 area Effects. Many draft-mirror candidates; keep SSH and cross-platform settings behavior intact. |
| Issue, PR, task pages | TaskPage.tsx, PullRequestPage.tsx, GitHubItemDialog.tsx, GitLabItemDialog.tsx, Linear components |
Inventory complete, manual review in progress | Large files with many Effects. Separate GitHub, GitLab, Linear, and generic review behavior. |
| Onboarding / feature wall | components/onboarding/**, components/feature-wall/** |
Inventory complete, manual review pending | Lower merge risk for pure visual/demo state, but avoid changing telemetry semantics. |
| Status, dashboard, activity | components/status-bar/**, components/dashboard/**, components/activity/** |
Inventory complete, manual review in progress | Status-bar account menu close reset covered by #3051. Check interval sharing, retained agent state, activity terminal portals. |
| Mobile app routes | mobile/app/** |
Inventory complete, manual review pending | 79 Effects, including large tasks.tsx and session route. Remote-client parity required. |
| Mobile shared source | mobile/src/** |
Inventory complete, manual review pending | Browser pane, transport client context, dictation hook, bottom drawer, new worktree modal. |
| Expo two-way audio hook | mobile/packages/expo-two-way-audio/src/hooks.ts |
Inventory complete, manual review pending | Single Effect plus useSyncExternalStore; verify native subscription cleanup. |
| Tests with hook mocks | *.test.ts, *.test.tsx, e2e comments |
Inventory complete, manual review pending | Do not count comment-only mentions as app Effect sites; update tests beside behavior changes. |
Current Findings Queue
These are candidate batches, not final conclusions. Each item needs code inspection before implementation.
| Candidate PR | Area | Symptom / wasted work to prove | Likely files | Merge risk |
|---|---|---|---|---|
| PR A | Small controlled-input resets | Extra render pass from Effects that mirror one prop into local draft state. | BrowserAddressBar.tsx covered by #3038; continue with BrowserFind.tsx, PdfFind.tsx, selected settings draft fields |
Low to medium |
| PR B | Settings draft hydration | Multiple independent Effects mirror persisted settings into draft state. | components/settings/** |
Medium |
| PR C | Browser pane Effect cluster | Large Effect cluster may mix external webview sync, derived UI state, and event-specific state repair. | BrowserPane.tsx, useGrabMode.ts, browser tabs |
High |
| PR D | Terminal tab repair and lifecycle | Effects repair active terminal/browser tab state after render; could cause extra render and focus churn. | Terminal.tsx, useTerminalTabs.ts, TerminalPane.tsx |
High |
| PR E | Right-sidebar polling and source-control state repair | PR checks, branch snapshots, file explorer watch/reveal may repeat work after unrelated state changes. | ChecksPanel.tsx, SourceControl.tsx, useGitStatusPolling.ts, file explorer hooks |
High |
| PR F | Mobile route monoliths | Mobile task/session routes have many chained Effects and timer/refetch patterns. | mobile/app/h/[hostId]/tasks.tsx, mobile/app/h/[hostId]/session/[worktreeId].tsx |
High |
| PR G | Mobile shared components | Visible/open Effects reset modal/drawer state and browser address state. | mobile/src/components/**, mobile/src/browser/MobileBrowserPane.tsx |
Medium |
| PR H | Editor search/preview/decorations | Debounce and scroll-restore Effects may be legitimate external sync but need cleanup/count review. | components/editor/** |
Medium to high |
| PR I | Feature wall/onboarding demos | Animation/demo Effects can often move to event handlers or tighter custom hooks. | components/feature-wall/**, components/onboarding/** |
Low to medium |
| PR J | GitHub filter controls | Extra render pass from mirroring parsed reviewer qualifier into local mode state. | PRFilterDropdowns.tsx covered by #3041 |
Low |
| PR K | Sidebar project filter | Extra render pass from mirroring the first filtered repo into command selection state. | SidebarFilter.tsx covered by #3042 |
Low |
| PR L | Diff note edit draft | Extra render pass from mirroring saved note body into edit draft while not editing. | DiffCommentCard.tsx covered by #3044 |
Low |
| PR M | Status-bar account menus | Extra render pass from closing Claude/Codex account submenus in Effects after the provider menu closes. | StatusBar.tsx covered by #3051 |
Low |
| PR N | Browser tab favicon fallback | Extra render pass from resetting failed favicon state in an Effect after tab favicon identity changes. | BrowserTab.tsx covered by #3052 |
Low |
| PR O | Workspace title rename draft | Redundant draft sync Effect runs while the inline title rename input is not mounted. | WorktreeTitleInlineRename.tsx covered by #3053 |
Low |
| PR P | Feature-wall tour static substep repair | Three no-op Effects scan static step arrays and repair ids that are only written from those arrays. | FeatureWallTourSurface.tsx covered by #3054 |
Low |
| PR Q | Repo combobox mount-open state | Mount-only auto-open path uses an Effect and ref guard instead of initializing state from the prop. | RepoCombobox.tsx covered by #3055 |
Low |
| PR R | Quick Open query reset | Extra render pass from clearing the Quick Open input after the dialog opens. | QuickOpen.tsx covered by #3056 |
Low |
| PR S | Project group dialog open resets | Extra render pass from seeding name/delete dialog local state after the dialog opens. | ProjectGroupNameDialog.tsx, ProjectGroupDeleteDialog.tsx covered by #3058 |
Low |
| PR T | Onboarding agent fallback disclosure | Extra render pass from opening the fallback agent list when a selected agent first appears there. | AgentStep.tsx covered by #3059 |
Low |
| PR U | Feature-wall tour workflow resets | Four local reset Effects run after workflow changes or close instead of in the selection/close path. | FeatureWallTourSurface.tsx covered by #3060 |
Low |
Merge Risk Scale
| Risk | Criteria | Required verification |
|---|---|---|
| Low | Isolated local UI state, no persistence, no IPC, no terminal/browser/source-control behavior. | Targeted unit test if available plus pnpm run typecheck:web. |
| Medium | Settings, editor UI, mobile local UI, or state persisted per workspace but no transport protocol changes. | Targeted tests plus manual interaction or focused Playwright/Electron check when UI behavior changes. |
| High | Terminal/PTY, browser webview, source control, mobile remote-client, SSH, polling, persistence, or provider behavior. | Targeted tests, pnpm run typecheck:web, and Electron or mobile/manual parity evidence as appropriate. |
PR Log
| PR | Branch | Area | Risk | Status | Evidence |
|---|---|---|---|---|---|
| #3038 | nwparker/react-perf |
Audit ledger plus browser address bar top-suggestion mirror Effect removal | Low | Merged | pnpm exec oxlint src/renderer/src/components/browser-pane/BrowserAddressBar.tsx; pnpm run typecheck:web. |
| #3041 | nwparker/react-perf-pr-filter |
PR reviewer filter mode derived from parsed query plus explicit user override | Low | Merged | pnpm exec oxlint src/renderer/src/components/github/PRFilterDropdowns.tsx; pnpm run typecheck:web. |
| #3042 | nwparker/react-perf-sidebar-filter |
Sidebar project filter command selection derived from filtered repos | Low | Merged | pnpm exec oxlint src/renderer/src/components/sidebar/SidebarFilter.tsx; pnpm run typecheck:web. |
| #3044 | nwparker/react-perf-low-risk-2 |
Diff note card removes saved-body-to-draft mirror Effect | Low | Merged | pnpm exec oxlint src/renderer/src/components/diff-comments/DiffCommentCard.tsx; pnpm run typecheck:web. |
| #3051 | nwparker/react-perf-low-risk-3 |
Status-bar account submenus collapse in provider menu open-change handlers | Low | Merged | pnpm exec oxlint src/renderer/src/components/status-bar/StatusBar.tsx; pnpm run typecheck:web. |
| #3052 | nwparker/react-perf-low-risk-4 |
Browser tab favicon failure reset happens during render for new favicon IDs | Low | Merged | pnpm exec oxlint src/renderer/src/components/tab-bar/BrowserTab.tsx; pnpm run typecheck:web. |
| #3053 | nwparker/react-perf-low-risk-5 |
Inline workspace-title rename removes inactive draft sync Effect | Low | Merged | pnpm exec oxlint src/renderer/src/components/sidebar/WorktreeTitleInlineRename.tsx; pnpm run typecheck:web. |
| #3054 | nwparker/react-perf-low-risk-6 |
Feature-wall tour removes static substep id repair Effects | Low | Merged | pnpm exec oxlint src/renderer/src/components/feature-wall/FeatureWallTourSurface.tsx; pnpm run typecheck:web. |
| #3055 | nwparker/react-perf-low-risk-7 |
Repo combobox initializes mount-open state without an Effect | Low | Merged | pnpm exec oxlint src/renderer/src/components/repo/RepoCombobox.tsx; pnpm run typecheck:web. |
| #3056 | nwparker/react-perf-low-risk-8 |
Quick Open clears its query on the open edge without a reset Effect | Low | Merged | pnpm exec oxlint src/renderer/src/components/QuickOpen.tsx; pnpm run typecheck:web. |
| #3058 | nwparker/react-perf-project-group-dialogs |
Project group dialogs reset local open-state during render | Low | Merged | pnpm exec oxlint src/renderer/src/components/sidebar/ProjectGroupDeleteDialog.tsx src/renderer/src/components/sidebar/ProjectGroupNameDialog.tsx; pnpm run typecheck:web. |
| #3059 | nwparker/react-perf-agent-step-latch |
Onboarding agent fallback disclosure latch updates during render | Low | Merged | pnpm exec oxlint src/renderer/src/components/onboarding/AgentStep.tsx; pnpm run typecheck:web. |
| #3060 | nwparker/react-perf-feature-tour-resets |
Feature-wall tour reset state moves into workflow/close transitions | Low | Open | pnpm exec oxlint src/renderer/src/components/feature-wall/FeatureWallTourSurface.tsx; pnpm run typecheck:web. |
Reproduction Commands
Run from the worktree root.
rg --files -g '*.tsx' -g '*.ts' | wc -l
rg -l "\\b(useEffect|useLayoutEffect|React\\.useEffect|React\\.useLayoutEffect)\\b" -g '*.tsx' -g '*.ts' | sort
The exact Effect-site counts above came from a TypeScript AST scan so comment-only mentions are not counted.