perf(board): memoize BoardCardView and BoardColumnView (#1151) #1155
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/agent-hooks!1155
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "code-lead/1151"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Wraps both
BoardCardViewandBoardColumnViewinReact.memoso the kanban skips re-rendering cards / columns whose props are referentially equal.board-card.tsx—BoardCardViewis nowmemo(BoardCardViewInner). All its props are primitives, enum strings, the stablecardobject (cards keep identity acrosspivotBoardResponse), or callbacks the parent already wraps inuseCallback, so default shallow equality is correct.board-column.tsx— same treatment forBoardColumnView.board.tsx— pulled the inlinerenderCard={(card) => <BoardCardView … />}arrow out of the.mapand into auseCallback. Without this, the column-levelReact.memowould be busted on every Board render because the arrow allocated a fresh function each tick. Also pre-computesmultiRepoonce instead of in the closure body.Test plan
just qapasses (typecheck + lint + format + sql / paraglide / flow checks)bun run test src/components/board/— 117 / 117 pass (no behaviour regressions in the existing card / column suites)Closes #1151
🤖 Generated with Claude Code
Review of perf(board): memoize BoardCardView and BoardColumnView
CI ✅ green | Tests ✅ 117/117 | Mergeable ✅
The approach is correct and the card-level memoisation does work. One issue defeats the column-level skip that the
useCallback(renderCard)extraction was specifically designed to enable — fix is a one-liner.✅ What's well done
React.memo+displayNameapplied cleanly to both components. DevTools will showBoardCardView/BoardColumnViewrather thanAnonymous.multiRepoas a boolean primitive before theuseCallbackis a nice touch: the callback only invalidates when the value flipstrue ↔ false, not wheneverresponsegets a new object reference on an SSE tick.❌
openAgentPickeris unstable — column-level memo is defeatedopenAgentPickercomes fromuseBoardKeymap()as a plain inline arrow in the hook's return object:This arrow is re-allocated on every render of the hook. Because
renderCardlistsopenAgentPickeras a dep:…
renderCardgets a new reference on every Board render,BoardColumnView.memoalways sees a changedrenderCardprop, and columns re-render unconditionally. The optimization the PR description claims — "SSE ticks that don't touch a column allow that column to skip re-rendering" — is never realised.Card-level memo still works (React reconciles per-element: even with a new
renderCard, the JSX nodes for unchanged cards have stable props, soBoardCardView.memofires correctly). That's a real win. But the column skip is the bigger saving on a 20+ card board.Fix — one line in
use-board-keymap.ts:setAgentPickerCardis a React state setter — guaranteed stable by React, so[]deps is correct and there is no stale-closure risk.useCallbackneeds to be added to the imports if it isn't already there.This single change makes
openAgentPickerstable →renderCardstable on SSE ticks that don't change selection/focus/drag →BoardColumnView.memofinally skips columns untouched by the patch.Minor observations (non-blocking)
cardKeyis called insiderenderCardbut is absent from the dep array. It's a stable module-level function so the omission is safe and ESLint exhaustive-deps won't flag it — just noting it in case a future reader wonders.selectedKeys,focusedKey,dragKey) will still bustrenderCardon every card interaction — that's expected and acceptable; the win is the SSE path.Good catch — you're right that the column-level skip was busted. Fixed in
d07a3c6.I went one step simpler than the suggested
useCallback: passingsetAgentPickerCarddirectly. React guarantees state setters are referentially stable, andDispatch<SetStateAction<BoardCardType | null>>is assignable to(card: BoardCardType) => voidby parameter contravariance (the setter's input type is strictly wider), so no wrapper is needed at all — one fewer allocation per hook render than wrapping inuseCallback.Left
closeAgentPicker/closeRerouteDialogas inline arrows: they bind thenullvalue the setter needs and aren't used as memo deps inBoard, so their identity doesn't matter for the SSE-tick path.just qa✅bun run test src/components/board/✅ 117/117APPROVED ✅
Two focused commits, 4 files, 60 insertions / 17 deletions. CI green.
What this does
BoardCardView—BoardCardViewInnerwrapped viaReact.memo(), exported asBoardCardViewwithdisplayNameset.BoardColumnView— same pattern forBoardColumnViewInner → BoardColumnView.renderCarduseCallback (board.tsx) — the inline arrow previously in the.map()created a new function reference on everyBoardrender, unconditionally bustingBoardColumnView.memo. Hoisting it intouseCallbackfixes that.multiRepoas a boolean primitive — computed before theuseCallbackdep array so the dep is stable when unchanged, not a live expression.openAgentPicker: setAgentPickerCard(use-board-keymap.ts) — replaces the(card) => setAgentPickerCard(card)wrapper arrow (new reference every hook render) with the React state setter directly (guaranteed stable by spec). This is load-bearing: without it,renderCard's dep onopenAgentPickerwould bust all column memos on everyBoardrender regardless of SSE content.Correctness
Dep array is complete —
[selectedKeys, focusedKey, dragKey, density, group, multiRepo, openAgentPicker]covers every closure capture inrenderCard. Nothing missing, nothing spurious.Card-level memo is genuinely effective — when
renderCardis stable (SSE tick that doesn't touch selection/focus/drag state),BoardColumnViewInnercallsrenderCard(card)and each resulting<BoardCardView>element skips re-render becausecardobject identity is preserved acrosspivotBoardResponseand all other props are primitives or stable references.Column-level memo fires on pure SSE ticks —
onDrop,onCardClick,onCardDragStart,onCardDragEndare alluseCallback-wrapped;onColumnMouseEnter=setFocusedColumnType(stable setter);renderCardstable as described. A patch affecting only sibling columns leaves all otherBoardColumnViewprops unchanged → those columns bail out.Selection changes still cause full re-renders — when
selectedKeyschanges, both theselectedKeysprop andrenderCard(which captures it) update, so all columns re-render. Unavoidable given the architecture; the optimisation correctly targets the hot SSE-only path.Minor nit (non-blocking)
use-board-keymap.tscomment reads "(PR #1155 review)" — slightly confusing since this is PR #1155. Likely meant(#1151)(the originating issue). No action needed.Clean, well-reasoned optimisation. The follow-up commit (
d07a3c6) closing theopenAgentPickerstability gap shows the dep chain was traced all the way through. Merging.