From 8ab638e48078e142e5c8bbe1ea2a1ebe20b84859 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 30 May 2026 13:43:10 +0000 Subject: [PATCH 1/6] perf(web): memoize list row components to prevent re-renders Wrap `RowView` and `Row` inside `EventList` with `React.memo` (using `areEqual` from `react-window` for the Row comparator). Also wrap `StatListRow` with `React.memo`. This reduces unnecessary React re-renders when parent lists or state updates. --- .jules/bolt.md | 4 ++++ packages/web/src/components/dashboard/event-list.tsx | 12 ++++++------ packages/web/src/components/dashboard/stat-list.tsx | 6 +++--- 3 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..8aa37e4 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,4 @@ + +## 2024-05-30 - [Memoizing List Components in react-window] +**Learning:** Virtualized lists (`react-window`) still re-render internal `Row` components when their parent updates. If the rows are complex or numerous (e.g. `EventList`), rendering them frequently can still cause performance degradation. +**Action:** Use `React.memo` with a custom comparator (e.g. `areEqual` from `react-window`) for rows passed to virtualized lists to stop unnecessary re-renders. Also apply `React.memo` to deeply nested simple row components like `StatListRow` if they are rendered frequently without prop changes. diff --git a/packages/web/src/components/dashboard/event-list.tsx b/packages/web/src/components/dashboard/event-list.tsx index 61a3f68..97dde79 100644 --- a/packages/web/src/components/dashboard/event-list.tsx +++ b/packages/web/src/components/dashboard/event-list.tsx @@ -1,5 +1,5 @@ -import { useMemo } from "react"; -import { List, type RowComponentProps } from "react-window"; +import { useMemo, memo } from "react"; +import { List, type RowComponentProps, areEqual } from "react-window"; import { User, Bot, Wrench, ChevronRight } from "lucide-react"; import { formatSlashCommandText, @@ -149,7 +149,7 @@ type RowViewProps = { chevron?: "collapsed" | "expanded"; }; -function RowView({ +const RowView = memo(function RowView({ label, preview, time, @@ -199,7 +199,7 @@ function RowView({ ); -} +}); type RowProps = { rows: FlatRow[]; @@ -209,7 +209,7 @@ type RowProps = { onToggleGroup: (firstIdx: number) => void; }; -function Row({ +const Row = memo(function Row({ index, style, rows, @@ -257,7 +257,7 @@ function Row({ /> ); -} +}, areEqual); export function EventList({ events, diff --git a/packages/web/src/components/dashboard/stat-list.tsx b/packages/web/src/components/dashboard/stat-list.tsx index 6e4ee42..d8e5890 100644 --- a/packages/web/src/components/dashboard/stat-list.tsx +++ b/packages/web/src/components/dashboard/stat-list.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from 'react' +import { memo, type ReactNode } from 'react' import { cn } from '@/lib/utils' interface StatListRowProps { @@ -16,7 +16,7 @@ const toneStyles: Record, string> = { muted: 'bg-muted', } -export function StatListRow({ +export const StatListRow = memo(function StatListRow({ icon, label, value, @@ -52,7 +52,7 @@ export function StatListRow({ ) -} +}) export function StatList({ children, From 055206dfbdfd914a1470a86caf75906d856ef53e Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 30 May 2026 14:41:01 +0000 Subject: [PATCH 2/6] perf(web): memoize list row components to prevent re-renders Wrap `RowView` and `Row` inside `EventList` with `React.memo` (using `areEqual` from `react-window` for the Row comparator). Also wrap `StatListRow` with `React.memo`. This reduces unnecessary React re-renders when parent lists or state updates. From 987caf5ffdb530dbf972ff170bbfbec2349c0ca1 Mon Sep 17 00:00:00 2001 From: "openai-code-agent[bot]" <242516109+Codex@users.noreply.github.com> Date: Sat, 30 May 2026 14:47:39 +0000 Subject: [PATCH 3/6] fix(web): memoize EventList rows without react-window areEqual Co-authored-by: seonghobae <8172694+seonghobae@users.noreply.github.com> --- .../src/components/dashboard/event-list.tsx | 132 +++++++++++++++--- 1 file changed, 114 insertions(+), 18 deletions(-) diff --git a/packages/web/src/components/dashboard/event-list.tsx b/packages/web/src/components/dashboard/event-list.tsx index 97dde79..3c1f49c 100644 --- a/packages/web/src/components/dashboard/event-list.tsx +++ b/packages/web/src/components/dashboard/event-list.tsx @@ -1,5 +1,5 @@ -import { useMemo, memo } from "react"; -import { List, type RowComponentProps, areEqual } from "react-window"; +import { useCallback, useMemo, memo } from "react"; +import { List, type RowComponentProps } from "react-window"; import { User, Bot, Wrench, ChevronRight } from "lucide-react"; import { formatSlashCommandText, @@ -127,22 +127,23 @@ function getSinglePreview(event: TimelineEvent): string { return event.toolName; } -function getIcon(event: TimelineEvent) { +function getIconParts(event: TimelineEvent) { if (event.kind === "message") { if (event.role === "HUMAN") { - return { Icon: User, bg: "bg-brand" }; + return [User, "bg-brand"] as const; } - return { Icon: Bot, bg: "bg-brand-2" }; + return [Bot, "bg-brand-2"] as const; } const isSpecial = event.isSkillCall || event.isAgentCall; - return { Icon: Wrench, bg: isSpecial ? "bg-chart-4" : "bg-muted-foreground" }; + return [Wrench, isSpecial ? "bg-chart-4" : "bg-muted-foreground"] as const; } type RowViewProps = { label: string; preview: string; time: string; - icon: ReturnType; + Icon: typeof User; + iconBg: string; isSelected: boolean; onClick: () => void; indented?: boolean; @@ -153,13 +154,13 @@ const RowView = memo(function RowView({ label, preview, time, - icon, + Icon, + iconBg, isSelected, onClick, indented = false, chevron, }: RowViewProps) { - const { Icon, bg } = icon; return (