Skip to content

perf(sessions-list): stop re-rendering every row on every session event - #1807

Open
f-liva wants to merge 3 commits into
slopus:mainfrom
f-liva:perf/sessions-list-rows
Open

f-liva wants to merge 3 commits into
slopus:mainfrom
f-liva:perf/sessions-list-rows

Conversation

@f-liva

@f-liva f-liva commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Summary

On a web tab left open for hours with dozens of streaming sessions, every socket event re-rendered every mounted row of the sessions list; an overnight heap reached 1.2 GB. Three per-row costs in sources/components/SessionsList.tsx and sources/components/FlatSessionRow.tsx add up to that: FlatSessionRow and SessionItem are React.memo'd, but every list rebuild hands them a freshly minted SessionRowData, so the default identity comparison failed for every row in the visible window on every event. Every row also mounted a SessionActionsPopover with visible={false}, which ran useSession, useSessionQuickActions, useWindowDimensions and the safe-area hook before returning null — a dozen store subscriptions and memos per row, all woken by every session update. And on web every row called useSessionActionAlert (sources/hooks/useSessionQuickActions.ts), paying a useSession subscription plus the whole quick-actions hook for an onLongPress handler that only native ever invokes.

This PR gives the two row memos a comparator that deep-equals the row data (flat primitives, so fast-deep-equal is cheap) together with the primitive props, mounts the popover only while the row holds an anchor (closing clears the anchor and unmounts it, as before), and picks a no-op useSessionActionAlert once at module load on web — Platform.OS is constant, so hook order stays stable and the native variant is unchanged.

Changes

  • FlatSessionRow / SessionItem: add memo comparators that compare selected/isFirst/isLast/isSingle and deep-equal the row data instead of relying on object identity
  • Mount SessionActionsPopover only while actionsAnchor is set, on both the flat and the grouped row
  • useSessionActionAlert: export a no-op variant on web, keep the native one as is

Test plan

  • pnpm typecheck (clean)
  • pnpm exec vitest run sources/components/HomeHeader.test.ts sources/components/sessionPresentation.test.ts sources/utils/flatSessionList.test.ts sources/utils/flatSessionRowPresentation.test.ts (the specs that cover the row presentation and list shape; all pass — the change is render-count only, no behavioural test exists for it)
  • Ran on a self-hosted web deployment: sessions list still updates thinking/unread/order; row actions popover (hover button and right-click) opens, closes and unmounts; command palette works

🤖 Generated with Claude Code

Federico Liva and others added 3 commits September 17, 2026 12:43
FlatSessionRow and SessionItem are memoized, but every list rebuild hands
them a freshly minted row object, so the identity comparison failed for
every mounted row on every socket event and the whole visible window
re-rendered. Row data is flat primitives; a fast-deep-equal comparator on
it (plus the primitive props) lets only the rows whose content changed
render again.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Every row mounted a SessionActionsPopover with visible=false, which ran
useSession, useSessionQuickActions, useWindowDimensions and the safe-area
hook before returning null — a dozen store subscriptions and memos per row,
all woken by every session update. Mount it only while the row holds an
anchor; closing clears the anchor and unmounts it, as before.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
The hook exists for the native onLongPress alert, but web rows called it
too and paid a useSession subscription plus the whole quick-actions hook
(useMachine, settings, a dozen callbacks) per row for a handler that is
never invoked. Pick a no-op variant once at module load on web —
Platform.OS is constant, so hook order stays stable — and keep the native
one unchanged.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

This branch has not been deployed

No deployments
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